Boost your GraphQL/Relay React app with Redux

Bhupendra Singh
scripbox-engineering
4 min readFeb 13, 2018

At Scripbox, we’ve been experimenting with GraphQL for internal purposes. We are huge fans of Redux. However, there is not much written about using the awesomeness of Redux alongside GraphQL. We have taken a stab at documenting our experience in marrying the development agility of GraphQL with painless state-management of Redux.

This post assumes we are fairly familiar with React and Redux.

Why GraphQL?

Clients often face the issue of over-fetching or under-fetching data. We need to have backing API endpoints for different representations of the same resource entity. This complexity is what GraphQL aims to solve.

GraphQL has inverted the equation in that, instead of a server dictating what attributes need to be served, the client gets to ask for information
that it needs.

As of now, Relay and Apollo Client are the prominent JS clients that can converse with a GraphQL backend.

We decided to go with Relay as the framework API looked quite straight-forward.

Relay

There are 3 key parts of integrating Relay with React:

  1. Fetching the schema from server
  2. Relay Environment
  3. QueryRenderer

Schema

GraphQL works on the basis of a shared schema between the server and client. We’re assuming we have the GraphQL server up and running. We need to ensure that we have the schema created as well.

Let us retrieve schema from the server.

$ npm install -g get-graphql-schema
$ get-graphql-schema __RELAY_API_ENDPOINT__ > ./schema.graphql

Now that we have the schema, let’s setup the Relay environment.

Environment

// src/createRelayEnvironment.jsconst {
Environment,
Network,
RecordSource,
Store
} = require('relay-runtime');
function fetchQuery(operation, variables) {
return fetch(___RELAY_API___, {
method: 'POST',
body: JSON.stringify({
query: operation.text,
variables,
}),
}).then(response => {
return response.json();
});
}
const network = Network.create(fetchQuery);
const source = new RecordSource();
const store = new Store(source);
export default new Environment({ network, store });

We can observe that Network and Store are parameters used to initialise the environment. The environment orchestrates API requests and manages resource state within the Relay boundary.

QueryRenderer

The QueryRenderer will be solely responsible for bridging the environment and our views. QueryRenderer is React-aware as it resides in views. It is also schema-aware, in that, the actual attributes to be fetched for a resource is
defined in the QueryRenderer.

Here is the QueryRenderer inside our LikeComponent.

import {
QueryRenderer,
GraphQL
} from 'react-relay';
import environment from '../src/createRelayEnvironment';
class LikeComponent extends Component {
render() {
return (
<div>
<QueryRenderer
environment={environment}
query={GraphQL`
query LikeQuery() {
user {
count
liked
}
}
`}
render={({ error, props }) => {
if (error) {
return <div>{ error.message }</div>;
} else if (props) {
JSON.stringify(props.user)
/>;
}
return <div className='loader'/>;
}}
/>
</div>
);
}
}

We are interested in fetching only count and liked for a user. Thanks to GraphQL, we don’t need to make any changes in the server to facilitate this as the schema created in the server already accounts for these attributes.

We need a way to tell the QueryRenderer what query to fire and how to handle the response. To address the former, we need to use a Query. We can notice the LikeQuery usage inside our QueryRenderer.

The Query abstraction prepares the attributes to be send across the network as required by GraphQL. This abstraction sits on top of Relay artefacts.

Relay artefacts are generated stubs that aids Query in preparing and sending the request payload. These artefacts are created by combing through the source files looking for *Query references and the schema.

Let us compile our Relay artefacts.

$ npm install relay-compiler
$ relay-compiler — src ./src — schema ./schema.GraphQL

At this point, we will find the artefacts under the __generated__ folder along with our QueryRenderer. We can inspect the artifacts to see how the GraphQL query is constructed.

State, state, more state!

Relay works well when managing client-server interactions; but not all interactions fall in this scope. We need state management for user-client interactions that might involve purely view changes where Relay has no
part to play. We can use the vanilla state given by React but that breaks the Flux philosophy.

Enter Redux

Redux helps us manage state in an immutable fashion and adheres to the Flux philosophy. As immutability is baked in, there are fewer chances of our application ending up in an invalid state. Let us see how we can make Redux play well with GraphQL.

// src/LikeComponent.js@connect(state => ({
counter: state.SampleReducer.get('counter'),
}))
class LikeComponent extends Component {
static propTypes = {
counter: PropTypes.number.isRequired,
// from react-redux connect
dispatch: PropTypes.func.isRequired,
}
render() {
const {
counter,
dispatch,
} = this.props;
return (
<div>
<QueryRenderer
environment={environment}
variables={{ after: (batchSize * (currentPage - 1)), first: batchSize }}
query={GraphQL`
query LikeQuery() {
user {
userId
count
liked
}
}
`}
render={({ error, props }) => {
if (error) {
return <div>{ error.message }</div>;
} else if (props) {
{ disptach(SampleAction.updateCounterValueWithServerValue(props.user.count)) }
<div className='Example'>
<p>Like: { counter }</p>
<button onClick={ () => dispatch(SampleActions.handleLikeButtonClick(props.user.liked))}>
Like
</button>
</div>
/>;
}
return <div className='loader'/>;
}}
/>
</div>
);
}
}

When our Component is initialised it invokes QueryRenderer to get state of liked and the count. When we click the Like button the reducers are called via the Actions which mutate the state of the View Component and calls the render function.

// src/LikeAction.jsupdateCounterValueWithServerValue(count) {
return {
type: UPDATE_COUNTER,
count
}
}
handleLikeButtonClick(liked) {
return {
type: UPDATE_LIKE_UNLIKE,
liked
}
}
// src/LikeReducer.js const initialState = Map({
counter: 0,
liked: false
});
const actionsMap = {
[UPDATE_LIKE_UNLIKE]: (state, action) => {
const counter = action.liked ? state.get('counter') - 1 : state.get('counter') + 1;
return state.merge(Map({
counter,
}));
},
[UPDATE_COUNTER]: (state) => {
const counter = state.get('count');
return state.merge(Map({
counter
}));
}
};

We have omitted the mutation call that updates the value in the server for brevity.

Conclusion

Our exploration is still in a nascent stage but we are optimistic about this approach. Please do let us know if we can improve this.

--

--