React with GraphQL — A better way
A custom hook on top of Apollo’s useLazyQuery for more control on GraphQL requests in your application and ensure smooth development experience.
Motivation
Today in the era of distributed and agile workflows, it is crucial to maintain coordination between teams spread across geographies and resolve dependencies on time to keep developers unblocked. Recently, while working on a react application we faced issues while integrating GraphQL APIs that were managed by a different team. Sometimes the server was down or APIs were partially working. In order to keep things going at our end we needed some sort of fallback setup to mock GraphQL requests with dummy data.
Below are some tried approaches:
- Static JSON files from file system: This method uses fetch to load static files from local directory. While this approach can render the UI with required data but require lot if rework in component source code and unit tests whenever we need to switch between mock data(fetch) and actual API(useQuery hook). Also, static files can not handle decision making like actual resolvers.
- Mirage JS: This is another solution to mock API requests but led to similar challenges as static files. No intelligence of resolvers and difference between nature of actual GraphQL requests and REST, which have single smart endpoint unlike REST.
Clearly, the solutions mentioned above are not easy to scale and maintain. We need some way to dynamically redirect GraphQL requests to different servers as required and control the same at individual request level. The sections below address the same problem.
Demo App
Assuming you are familiar about GraphQL basics, I’ll skip theories and jump straight into the implementation. The source code is available @git.
There are two identical GraphQL servers which holds GraphQL setup for users and accounts. Consider server1 as production server and server2 as fallback mock server. Local file system is used to store JSON files as database.
A basic react app is initialised in a separate directory demo. There is a sub-directory in demo/src with name graphql that contains all the GraphQL related files to consume data from server. It has Apollo Client instance and queries for getting users and accounts. Ignore other files for now.
The selection of different servers can be controlled at two levels:
- App level: An env variable API_MOCK is used to decide the value of boolean request header mock. If true, queries are sent to mock server. This approach will control all queries in the application at one place . It will take one of three possible values: ‘ON’, ‘OFF’, ‘AUTO’. The last mode will automatically redirect to the fallback server in case main server throws error.
- Query level: The file queryMock.js keeps key value pairs to direct any particular query to a different server. Key should exactly match the query name in schema. Value is a boolean.
Apollo-Client setup
The client is initialised with apollo-link-http custom fetch option. All the requests to GraphQL server are intercepted at this level to decide the domain dynamically based on headers or query level mocks.
Custom Hook — useQueryMod
This is a wrapper on top of useLazyHook provided by Apollo. It checks environment variable API_MOCK to set query header mock. It has same interface as useLazyQuery hook and handles redirection to mock server behind the scenes. The will ease the integration with existing applications with minimal source code changes. Another benefit of using a custom hook is the ease of unit testing because we can directly mock the custom hook module using jest and there is no need to wrap the test instance in Apollo Context. It is also possible to use fetchOptions property instead of headers to set custom properties in fetch. Refer Apollo docs for more details.
The ability to dynamically select the server at individual query level is very powerful pattern. It gives better control over the GraphQL requests along with other benefits such as ease of unit testing. The pattern solves the problem of mocking data for development and testing environments as well as switching between different servers (e.g. prod, it, uat) in production ready applications.