How to connect a React component to its source of data using local GraphQL server?
- Modify GraphQL local schema in
app/graphql/schema.graphql - Frontend:
- Create a
queries@local.gqlin component folder - Write necessary queries in
queries@local.gql - Register your documents in codegen config (
codegen-local.yml) - Run
yarn run gql-gento generatequeries@local.gql.generated.tsx - Use the components of
queries@local.gql.generated.tsxin the React component to connect data
- Create a
- Backend:
- Run
yarn run gql-gento updateapp/graphql/resolvers-types.generated.ts - Create a
resolvers.tsnext to code for the source of data - Write necessary resolvers in
resolvers.tsand useresolvers-types.generated.tsfor typing - Add the new resolvers in
app/graphql/allResolvers.ts
- Run
Let's say we have a React component DummyStatus and we want to connect the value of working.
// in app/dummy/DummyStatus.tsx
export interface Props {
working: boolean
}
export default class DummyStatus extends React.PureComponent<Props, {}> {
render() {
// we want to connect the `working` prop to its source of data
const { working } = this.props;
return (
<p>I'm {working ? 'working' : 'not working'}</p>
);
}
}Modify the GraphQL local schema in app/graphql/schema.graphql:
type Query {
"""Is launch at startup enabled or not."""
autoLaunchEnabled: Boolean
+
+ """Return true if dummy is working"""
+ isDummyWorking: Boolean!
}Note: if you are adding a big chunk of schema, consider using schema import Note: see also Apollo: understanding schema concepts
Create a app/dummy/queries@local.gql that'll hold the necessay queries:
query GetDummyStatus @live @local {
isDummyWorking
}Note: see also GraphQL: learn queries
Register your queries in codegen config codegen-local.yml to generate a query components in app/dummy/queries@local.gql.generated.tsx:
+ ./app/dummy/queries@local.gql.generated.tsx:
+ documents: ./app/dummy/queries@local.gql
+ plugins:
+ - typescript
+ - typescript-operations
+ - typescript-react-apolloRun the generator: $ yarn run gql-gen.
Connect the React component with the generated providers:
+import { GetDummyStatus } from './queries@local.gql.generated';
...
-export default class DummyStatus extends React.PureComponent<Props, {}> {
+class DummyStatus extends React.PureComponent<Props, {}> {
...
}
+const withWorkingState = withGetDummyStatus({
+ props: ({ data }) => ({
+ // map data to props
+ working: data && Boolean(data.isDummyWorking),
+ }),
+});
+
+export default withWorkingState(DummyStatus);Note: look at Apollo's "using typescript" for more tips
Run yarn run gql-gen to update the resolversapp/graphql/resolvers-types.generated.ts
Create a app/dummy-data/resolvers.ts:
import { IResolvers } from '../graphql/resolvers-types.generated';
const resolvers: IResolvers = {
Query: {
isDummyWorking: (obj, args, context) => {
return true;
},
},
};
export default resolvers;Add the new resolvers in app/graphql/allResolvers.ts:
+import dummyResolvers from '../dummy-data/resolvers';
/**
@@ -8,4 +9,6 @@ import { GraphQLSchema } from 'graphql';
export function addAllResolvers(schema: GraphQLSchema) {
+ addResolveFunctionsToSchema({ schema, resolvers: dummyResolvers });
}You can declare a schema in a separate file and import it in the main schema file.
Modify the GraphQL local schema in app/graphql/schema.graphql:
+# import Query.*, Mutation.* from "../dummy/schema.graphql"
+
type Query {
"""Is launch at startup enabled or not."""
autoLaunchEnabled: Booleanand in app/dummy/schema.graphql:
type Query {
"""Return true if dummy is working"""
isDummyWorking: Boolean!
}see graphql-import
Resolvers can return a value, a Promise, but also a RxJS's Observable. In this case, the data on the front-end will be automatically updated with the observable value.
import { timer } from "rxjs";
const resolvers = {
Query: {
// resolvers can return an Observable
time: () => {
// Observable that emits increasing numbers every 1 second
return timer(1000, 1000);
}
}
};Note: see reactive-graphql
GraphQL resovers' context have a reference to the redux store. You can use subscribeStore to resolve an observable which value corresponds to the given state's selector and that will be updated on change.
import { subscribeStore } from '../utils/observable';
import { getAppAutoLaunchEnabledStatus } from './selectors';
const resolvers: IResolvers = {
Query: {
autoLaunchEnabled: (_obj, _args, context) => {
return subscribeStore(context.store, getAppAutoLaunchEnabledStatus);
},
},
}The typing of the generated provider components and resolvers is infered from GraphQL schema and queries. Use this to help development!
Note: see also Apollo's "using typescript"
Use Apollo Client Devtools to inspect queries and the schema.
In Bx, you sometimes need to close and reopen the devtool to see the Apollo tab appear.
We use apollo-storybook-decorator to connect Apollo to Storybook.
This decorator uses the schema (app/graphql/schema.graphql) and mocks resolvers automatically.
Everything is already set up in the .storybook/config file and the stories don't need anything special.
- Create a
stories.tsxfile in the component folder - Write the stories you need:
import * as React from 'react';
import { storiesOf } from '@storybook/react';
import MyComponent from './MyComponent';
storiesOf('Component|MyComponent ', module)
.add('My Component - Normal', () => {
return (
<MyComponent />
);
});See also Apollo Mocking
If you want custom mocking, you need to pass it in the decorator mock options in Storybook config.
You can gather the different queries and mutations necessary for a feature or a component in a single queries@local.gql file. The generator will generate several components.
To query the the API api.getstation.com, in your queries, omit the @local directive in your query.
You can use the code-generation as well. That's the same principle as described above, except:
- make sure the API schema is up to date:
-- to update from the production API:
npx get-graphql-schema https://api.getstation.com/graphql > api-schema.graphqls-- to update from the local dev API:npx get-graphql-schema http://localhost:4001/graphql > api-schema.graphqls - you'll need to reference to your documents in the
codegen.yamland notcodegen-local.yaml - the convention is
queries.gqland notqueries@local.gql
Compose several queries and mutaiton before connecting to the react component.
Example:
import * as React from 'react';
import { compose } from 'redux';
import { GetAutolaunchStatus, EnableAutoLaunch } from './queries@local.gql.generated';
class SettingsAutoLaunch extends React.Component<Props, {}> {
....
}
const connect = compose(
withGetAutolaunchStatus<{}, Partial<Props>>({
props: ({ data }) => ({
loading: !data || data.loading,
isAutoLaunchEnabled: !!data && Boolean(data.autoLaunchEnabled),
}),
}),
withEnableAutoLaunch<{}, Partial<Props>>({
props: ({ mutate }) => ({
onEnableAutoLaunch: (enabled: boolean) => mutate && mutate({ variables: { enabled } }),
}),
})
);
export default connect(SettingsAutoLaunch);- Can't use fragments
- Make sure to use
@live(and@local) directives, otherwise the results will not be updated
Being able to make faster iteration on the UI of Station.
This motivation led to these requirements:
- Decouple data logic from UI logic (back-end vs front-end)
- Have a clear and well-defined interface between data and UI
- Being able to mock data logic to work on UI part only
- Being able to dev UI outside app (a la storybook)
- Get closer to web dev standard stack to use eco-system and community experience
- (Almost) perfect typing without much effort
A large part of the stack use classic GraphQL tools and architecture of a schema being executed locally.
The few "exostisms" lies in:
- reactive resolvers: as noted above, we can use reactive resolvers. This is possible because queries are executed against a particular GraphQL implementation that supports reactive resolvers (reactive-graphql). In future, we hope this will be stadardized in GraphQL standard implementation.
- directive-based routing: we have 2 query executors: local resolvers and the remote API. The queries are routed one or the other using the directive
@localon the query. This is implemented thanks to Apollo links. SeesplitLocalAndAPI. In the future, we'd prefer using schema stitching. - transfer queries from renderer to main: the client is on the renderer side while the query execution runs on the main-side.
- reactive-graphql: GraphQL reactive executor (observable resolver).
- apollo-link-reactive-schema: Apollo Link that provides a reactive-graphql execution environment to perform operations on a provided reactive schema.
- graphql-code-generator: used to generate resolvers typings and provider components.
- graphql-codegen-typescript-reactive-resolvers: plugin for graphql-code-generator that supports reactive resolvers.
- react-apollo: GraphQL client for React.
- apollo-storybook-decorator: use storybook with GraphQL (does the mocking).
- Apollo Links and friends: to do some piping.
- graphql-import: to cut schema in different files