Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Data Module: Expose State using a GraphQL API #4083

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 67 additions & 1 deletion data/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@
* External dependencies
*/
import { createStore, combineReducers } from 'redux';
import { flowRight } from 'lodash';
import { flowRight, merge } from 'lodash';
import { ApolloLink, Observable, execute as executeLink } from 'apollo-link';
import { execute } from 'graphql';
import { makeExecutableSchema } from 'graphql-tools';

/**
* Internal dependencies
*/
import createQueryHigherOrderComponent from './query';

/**
* Module constants
Expand Down Expand Up @@ -32,3 +40,61 @@ export const subscribe = store.subscribe;
export const dispatch = store.dispatch;

export const getState = store.getState;

const schemas = [ `
type Query {
hello: String
}
` ];

const resolvers = [ {
Query: { hello: () => 'dolly' },
} ];

let schema = makeExecutableSchema( {
typeDefs: schemas[ 0 ],
resolvers: resolvers[ 0 ],
} );

/**
* Registers a sub GraphQL schema
*
* @param {String} registeredSchema The GraphQL schema to register
* @param {Object} registeredResolver The GraphQL resolver to register for this schema
*/
export function registerSchema( registeredSchema, registeredResolver ) {
schemas.push( registeredSchema );
resolvers.push( registeredResolver );
schema = makeExecutableSchema( {
typeDefs: schemas,
resolvers: merge( ...resolvers ),
} );
}

const graphLink = new ApolloLink( ( operation ) => {
return new Observable( observer => {
return store.subscribe( () => {
const result = execute(
schema,
operation.query,
null,
{ state: store.getState() },
operation.variables,
operation.operationName
);
if ( result.data || result.errors ) {
observer.next( result );
} else {
result.then( observer.next.bind( observer ) );
}
} );
} );
} );

const client = {
query: ( operation ) => {
return executeLink( graphLink, operation );
},
};

export const query = createQueryHigherOrderComponent( client );
66 changes: 66 additions & 0 deletions data/query.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor, it might be located next to other HOCs inside @wordpress/components.

* External Dependencies
*/
import { isFunction } from 'lodash';

/**
* WordPress Dependencies
*/
import { Component } from '@wordpress/element';

const createQueryHigherOrderComponent = ( client ) => ( mapPropsToQuery, mapPropsToVariables = () => ( {} ) ) => ( WrappedComponent ) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would these more accurately be mapQueryToProps and mapVariablesToProps ?

Or am I wrong in thinking that these functions would take values from the query result and map them to props for the component?

Copy link
Contributor Author

@youknowriad youknowriad Dec 19, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, they are correctly names, these are useful if you want to create custom queries based on props.

I can see something like mapResultToProp being usefull as well. It could be a third optional callback.

Edit In a GraphQL API, this is less important though because you already ask for the data you want, it could be convenient though for "undefined" checks and stuff like that.

return class GraphQueryComponent extends Component {
constructor() {
super( ...arguments );
this.state = {
data: null, errors: null,
};
}

componentDidMount() {
this.buildQuery( this.props );
this.request();
}

componentWillUnmount() {
this.cancelRequest();
}

componentWillReceiveProps( newProps ) {
this.buildQuery( newProps );
this.request();
}

buildQuery( props ) {
if ( isFunction( mapPropsToQuery ) ) {
this.query = mapPropsToQuery( props );
} else {
this.query = mapPropsToQuery;
}
this.variables = mapPropsToVariables( props );
}

cancelRequest() {
if ( this.unsubscribe ) {
this.unsubscribe();
}
}

request() {
this.cancelRequest();
const query = client.query( { query: this.query, variables: this.variables } );
const observer = query.subscribe( ( results ) => {
this.setState( results );
Copy link
Member

@atimmer atimmer Dec 20, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In your example in the pull request description, the data is accessed using props.data, but in the higher order component, you only set the state. How does this work?

Copy link
Contributor Author

@youknowriad youknowriad Dec 20, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, actually a GraphQL result is always like this { data, errors } so we'll end up with these two props.

For convenience, we could add an optional callback to "polish" the props like suggested by @aduth. mapResultsToProps

} );
this.unsubscribe = observer.unsubscribe;
}

render() {
return (
<WrappedComponent { ...this.props } { ...this.state } />
);
}
};
};

export default createQueryHigherOrderComponent;
35 changes: 35 additions & 0 deletions editor/store/data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Internal Dependencies
*/
import { getEditedPostTitle } from './selectors';

export const schema = `
type CoreEditedPost {
title: String
}

type CoreEditor {
post: CoreEditedPost
}

extend type Query {
editor: CoreEditor
}
`;

export const resolver = {
Query: {
editor: ( _, args, context ) => ( {
post: () => ( {
title() {
const state = context.state[ 'core/editor' ];
return getEditedPostTitle( state );
},
} ),
} ),
},

CoreEditor: editor => editor,

CoreEditedPost: post => post,
};
4 changes: 3 additions & 1 deletion editor/store/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* WordPress Dependencies
*/
import { registerReducer } from '@wordpress/data';
import { registerReducer, registerSchema } from '@wordpress/data';

/**
* Internal dependencies
Expand All @@ -11,13 +11,15 @@ import reducer from './reducer';
import { withRehydratation, loadAndPersist } from './persist';
import enhanceWithBrowserSize from './browser';
import store from './store';
import { schema, resolver } from './data';

/**
* Module Constants
*/
const STORAGE_KEY = `GUTENBERG_PREFERENCES_${ window.userSettings.uid }`;

registerReducer( 'core/editor', withRehydratation( reducer, 'preferences' ) );
registerSchema( schema, resolver );
loadAndPersist( store, 'preferences', STORAGE_KEY, PREFERENCES_DEFAULTS );
enhanceWithBrowserSize( store );

Expand Down
2 changes: 1 addition & 1 deletion lib/client-assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ function gutenberg_register_scripts_and_styles() {
wp_register_script(
'wp-data',
gutenberg_url( 'data/build/index.js' ),
array(),
array( 'wp-element' ),
filemtime( gutenberg_dir_path() . 'data/build/index.js' )
);
wp_register_script(
Expand Down
Loading