Usage

November 20, 2023 ยท View on GitHub

How to connect a React component to its source of data using local GraphQL server?

Checklist

  1. Modify GraphQL local schema in app/graphql/schema.graphql
  2. Frontend:
    1. Create a queries@local.gql in component folder
    2. Write necessary queries in queries@local.gql
    3. Register your documents in codegen config (codegen-local.yml)
    4. Run yarn run gql-gen to generate queries@local.gql.generated.tsx
    5. Use the components of queries@local.gql.generated.tsx in the React component to connect data
  3. Backend:
    1. Run yarn run gql-gen to update app/graphql/resolvers-types.generated.ts
    2. Create a resolvers.ts next to code for the source of data
    3. Write necessary resolvers in resolvers.ts and use resolvers-types.generated.ts for typing
    4. Add the new resolvers in app/graphql/allResolvers.ts

By example

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>
    );
  }
}

Schema

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

On the front-end

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-apollo

Run 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

On the back-end

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 });
 }

Recipes

Schema import

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: Boolean

and in app/dummy/schema.graphql:

  type Query {
    """Return true if dummy is working"""
    isDummyWorking: Boolean!
  }

see graphql-import

Reactive resolvers

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

Use redux-store in resolvers

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);
    },
  },
}

Typing will guide you

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 dev tool

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.

Mocking and Storybook

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.

  1. Create a stories.tsx file in the component folder
  2. 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.

Multiple queries (and mutations) in documents

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.

Working with the remote API

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.yaml and not codegen-local.yaml
  • the convention is queries.gql and not queries@local.gql

HOC composition

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);

Known caveats

  • Can't use fragments
  • Make sure to use @live (and @local) directives, otherwise the results will not be updated

Developers

Motivation

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

Architecture overview

A large part of the stack use classic GraphQL tools and architecture of a schema being executed locally.

"Exostisms"

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 @local on the query. This is implemented thanks to Apollo links. See splitLocalAndAPI. 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.

Tech stack