This is opinionated replacement for graphql
decorator from react-apollo
package.
Under development, API can change
npm install --save react-apollo-graphql
It provides:
- simple error handling on the component level
- readable passing of queries' results to your component
- typed props and render props using flow type
- server side render
import GraphQL from 'react-apollo-graphql';
import ApolloClient from 'apollo-client';
import ApolloProvider from 'react-apollo';
<ApolloProvider client={new ApolloClient(...)}>
<GraphQL render={(queries, mutations, props) => <div />} />
</ApolloProvider>
import GraphQL from 'react-apollo-graphql';
import ApolloClient from 'apollo-client';
<GraphQL client={new ApolloClient(...)} render={(queries, mutations, props) => <div />} />
In order to use define and use queries, one has to initialize them.
// @flow
import type { QueryInitializerOptions } from 'react-apollo-graphql';
import type { ApolloClient, ObservableQuery } from 'react-apollo-graphql/lib/types';
const queries = {
// queryA will be resolved only once
queryA: (
client: ApolloClient,
props: Object
): ObservableQuery<{ id: number }> => client.watchQuery({
query: gql`{ id }`,
}),
// queryB will be resolved everytime the relevant props change
queryB: (
client: ApolloClient,
props: Object,
options: QueryInitializerOptions
): ObservableQuery<{ name: string }> => {
// add our function which will be called on every props change
options.hasVariablesChanged((currentProps, nextProps) => {
if (currentProps.name === nextProps.name) {
return false;
}
return { name: nextProps.name };
});
return client.watchQuery({
query: gql`query test($name: String!) { id(name: $name)}`,
variables: { name: props.name },
});
}
};
<GraphQL
queries={queries}
render={(initializedQueries) => {
console.log(initializeQueries.queryA.data);
console.log(initializeQueries.queryA.loading);
console.log(initializeQueries.queryA.error);
console.log(initializeQueries.queryA.networkStatus);
console.log(initializeQueries.queryA.partial);
}}
/>
In order to define and use mutations, one has to provided initializers. Initializers are called on every render so you have current props
available in the initializers.
// @flow
import type { ApolloClient, QueryResult } from 'react-apollo-graphql/lib/types';
const mutations = {
registerUser: (
client: ApolloClient,
props: Object
) => (): Promise<QueryResult<{ registerUser: boolean }>> => client.mutate({
mutation: gql`mutation registerUser($email: String!) { registerUser(email: $email) }`,
variables: {
email: props.email,
},
}),
};
<GraphQL
email="test@test.com"
mutations={mutations}
render={(queries, mutations, fetchers, props) => {
mutations.registerUser(props.email).then(
(data) => console.log(data.registerUser),
e => console.error(e),
);
}}
/>
In order to use fetchers (queries that run only when user invokes them), user has to first initialize them. Fetchers are initialized with client
and current props
on each render and passed to render()
function.
// @flow
import type { QueryInitializerOptions } from 'react-apollo-graphql';
import type { ApolloClient, QueryResult } from 'react-apollo-graphql/lib/types';
const fetchers = {
// queryA will be resolved only once
search: (
client: ApolloClient,
props: Object
) => (term: string): Promise<QueryResult<Array<{ id: number }>>> => client.query({
query: gql`query search($term: String!) { search(term: $term) { id } }`,
variables: { term },
}),
};
<GraphQL
fetchers={fetchers}
text="text"
render={(queries, mutations, fetchers, props) => {
fetchers.search(props.text).then(
(data) => console.log(data.search[0].id);
);
}}
/>
In order to use fragments (you can simulate partial results using fragments), user has to first initialize them. Fragments are initialized with client
, previous props
and current props
on componentWillMount
and every update if props
used by given fragment have changed. If props have not changed and you don't want fragment to fetch data on every update, return false
.
// @flow
import type { QueryInitializerOptions } from 'react-apollo-graphql';
import type { ApolloClient, FragmentResult } from 'react-apollo-graphql/lib/types';
const fragments = {
// user detail will be resolved on componentWillMount and on every update if props
// used as variables have changed
userDetail: (
client: ApolloClient,
previousProps: ?Object,
currentProps: Object
): FragmentResult<{ __typename: 'User', id: number, name: string }> => {
if (previousProps && previousProps.id === currentProps.id) {
return false;
}
return client.readFragment({
id: `User:${currentProps.id}`,
fragment: gql`fragment userDetails on User { __typename, id, name }`,
});
}
};
<GraphQL
fragments={fragments}
id={10}
render={(queries, mutations, fetchers, fragments, props) => {
expect(fragments.userDetail).toEqual({
__typename: 'User',
id: 10,
name: 'Fero',
});
}}
/>
For server side rendering you need to:
- import helper as
import { getDataFromTree } from 'react-apollo-graphql';
- instantiate your view (
const view = <App />;
) - wait for all queries to be resolved
await getDataFromTree(view);
- render view
ReactDOM.renderToString(view);
- profit (but you have to hydrate your apollo store on the client side 😉 )
// example taken from react-router v4 docs
import { createServer } from 'http';
import ApolloClient from 'apollo-client';
import ApolloProvider from 'react-apollo';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import { StaticRouter } from 'react-router';
import { getDataFromTree } from 'react-apollo-graphql';
import App from './App';
createServer(async (req, res) => {
const context = {};
const client = new ApolloClient();
const view = (
<StaticRouter
location={req.url}
context={context}
>
<ApolloProvider client={client}>
<App/>
</ApolloProvider>
</StaticRouter>
);
await getDataFromTree(view);
const html = ReactDOMServer.renderToString(view);
if (context.url) {
res.writeHead(301, {
Location: context.url
});
res.end();
} else {
res.write(`
<!doctype html>
<div id="app">${html}</div>
`);
res.end();
}
}).listen(3000);
- apollo client is provided from
apollo-client
package. See documentation.
// @flow
export type QueryInitializerOptions = {
// sets function to determine if there is a relevant change in props to compute new variables
// returns false if there is no change in props used for variables
// or returns new variables for query.setVariables()
hasVariablesChanged: (
(currentProps: Object, nextProps: Object) => boolean | { [key: string]: any },
) => void,
};
<GraphQL fetchers?={Fetchers} fragments?={Fragments} queries?={Queries} mutations?={Mutations} render={RenderFunction} />
Fragments = { [key: string]: (client: ApolloClient, previousProps: ?Object, currentProps: Object) => FragmentResult<any> }
optional
prop, object with fragments' initializer- each initializer will be initialized with Apollo client, previousProps and props passed to the initializer on each mount and update
- each initializer is update only if it does not return false
- each initializer has to return
false
or result ofclient.readFragment()
(this means that it has to call theclient.readFragment() method
)
Fetchers = { [key: string]: (client: ApolloClient, props: Object) => (...args: any) => Promise<QueryResult<*>>}
optional
prop, object with fetchers' initializer- each initializer will be initialized with apollo client and props passed to the initializer on each render (on mount and every update)
- each initializer has to return
(...args: any) => Promise<QueryResult<*>>
(this means that it has to call theclient.query() method
)
Queries = { [key: string]: (client: ApolloClient, props: Object, options: QueryInitializerOptions) => ObservableQuery<*> }
optional
prop, object with query initializers.- each initializer will be initialized with apollo client and props passed to initializer on component mount
- each initializer has to return
ObservableQuery
(this means that it has to call theclient.watchQuery() method
)
Mutations = { [key: string]: (client: ApolloClient, props: Object) => () => Promise<QueryResult<*>>}
optional
prop, object with mutation initializers- each initializer will be initialized with apollo client and props passed to the initializer on each render (on mount and every update)
- each initializer has to return
() => Promise<QueryResult<*>>
(this means that it has to call theclient.mutate() method
)
RenderFunction = (queries: InitializedQueries, mutations: InitializedMutations, fetchers: InitializedFetchers, props: Object) => React$Element<any>
- called on mount and updates
queries
arg: result of each query initializer passed to thequeries
prop on<GraphQL />
component will be mapped to it's result, plus additional methods likefetchMore(), refetch(), etc
seeclient.watchQuery() method
mutations
arg: each mutation initializer from themutations
prop passed to the<GraphQL />
component will be called on render and the result will be passed under the samekey
to themutations
arg of render function.fetchers
arg: each fetcher initializer from thefetchers
prop passed to the<GraphQL />
component will be called on render and the returned function will be passed under the samekey
to thefetchers
arg of render function.props
arg: current props passed to<GraphQL />
component