Reconciling Relay With Your Redux App


It’s not news how popular React has become over the past few years. Many developers have praised such benefits as the speed that comes from the virtual DOM, the ease of developing an app as a set of reusable components and one-way data flow. What hasn’t been as clear-cut is which frameworks people have chosen to use in their React development, and why. Here at 1stdibs, we have been converting portions of our code from Backbone to React for over a year. In the past we’ve used React with Fluxible, but we’ve have been moving exclusively to Redux as of late. Redux is easy to learn, results in clean and consistent code, and provides for a great debugging experience. We’ve also been busily moving some client concerns to our GraphQL server. This allows us to port our Backbone models and collections to the React ecosystem without sacrificing how we have modularized our data objects. You can read why we decided to use GraphQL for representing and manipulating our data.

Our next step was to use Relay to incorporate this functionality into our applications. Here is where a problem arose - there are many examples and tutorials online for using Relay and GraphQL, and for using Redux, but not a lot for using both together. Redux helps us maintain application state (e.g. the inputs entered by a user) and define how the application will respond to events, while Relay (in conjunction with GraphQL) allows us to interact with our backend - that is, to fetch and update data. These frameworks and their benefits do not have to be mutually exclusive. In fact, using them together can provide a nice separation of concerns that is so important in web development, as we will see shortly.

A Simple Pattern for Using Relay and Redux Together

Luckily, it turns out the basics of using the two together is not that complex. Both Redux and Relay work by defining a higher-order component that wraps a regular (presentational) React component. We have found that we can use the two together by implementing a 3-tier structure: our presentational component is first wrapped by a Redux container, which is then wrapped by a Relay container. In the outermost level we have our Relay container for fetching data. This data is then passed via props to the Redux container. This component can then use these props and any other props passed in from further up in the tree. This data can then simply be passed further along to the presentational component if needed, or used as part of dispatching actions that will occur on the page. Here is where we see the separation of concerns I mentioned earlier - the presentational component handles the display, while the Relay and Redux containers handle the business logic. This logic is then further separated - logic surrounding the behavior of the page is defined by Redux, while logic for fetching and updating data is handled by Relay.

Here’s a simplified example illustrating this wrapping pattern:

Presentational Component (lowest level) - ItemView.jsx

const React = require('react');

const ItemView = ({viewer, imgSources, onItemImagesClick}) => {
    return (
        <div>
            // markup for component utilizing props above
        </div>
    );
};

ItemView.propTypes = {
    viewer: React.PropTypes.object,
    imgSources: React.PropTypes.array,
    onItemImagesClick: React.PropTypes.func
};

module.exports = ItemView;

Redux container (page state) - ItemReduxComponent.jsx

const React = require('react');
const { connect } = require('react-redux');
const ItemView = require('./ItemView');
const actions = require('../../actionCreators/actions');
const { photoGetters } = require('../helpers/getters');

const mapStateToProps = (state, ownProps) => {
    return {
        imgSources: photoGetters.getPhotoPaths(ownProps.viewer.items)
    };
};

const mapDispatchToProps = (dispatch) => {
    return {
        onItemImageClick: itemId => dispatch(actions.doItemClick(itemId))
    };
};

module.exports = connect(mapStateToProps, mapDispatchToProps)(ItemView);

Relay container (server state) - ItemRelayContainer.jsx

const React = require('react');
const Relay = require('react-relay');
const ItemReduxComponent = require('./ItemReduxComponent');

module.exports = Relay.createContainer(ItemReduxComponent, {
    fragments: {
        viewer: () => Relay.QL`
            fragment on Viewer {
                items {
                    id
                    status
                    photos {
                        webPath
                    }
                }
            }
        `
    }
});

This example is highly simplified (a lot of code is not shown for brevity), but demonstrates using one component to fetch an item’s data, another to perform logic on this data, and finally a component for display. The query in ItemRelayContainer is performed against our GraphQL server, meaning that the requested data is fetched, either from a cache or from our backend API. The data that comes back is passed as props to ItemReduxComponent, since that is the component specified as the first argument to Relay.createContainer. More specifically, the data is stored in props.viewer, as viewer is the name of the query fragment in our Relay container. We queried for items data, so the returned items are within that viewer object, and can be accessed as props.viewer.items. ItemReduxComponent then defines some extra props. These extra values are either more data calculated using our business logic code (for instance, getting a full path for imgSources) or functions defining how events are going to be handled (onItemImagesClick). By using the Redux connect function, the original props passed to ItemReduxComponent (which we saw included viewer) are combined with the new props created and are all passed to ItemView.

For the basics, this works. Unfortunately for this setup (but fortunately for our business), we have lots of items, more than can be displayed on one page. This requires us to use query params to filter on items, adding a complicated wrinkle to using Relay and Redux together. Why is this a problem?

Incorporating Query Parameters

Normally in Redux, an action will cause a reducer to make a new instance of the application’s state. If this causes the props on a component to be updated, the components will get re-rendered. Similarly, Relay containers refetch data when params to the GraphQL query are updated. These params are stored in this.props.relay.variables and are updated using the this.props.relay.setVariables function. The resulting data returned is different, potentially causing components to be refreshed. The complication now is that we are concerned with data in two places - in the Redux store, and in the props.relay.variables object. As an example, let’s assume a user wanted to click a link to view a 2nd page of items, where page is a param we use in our GraphQL query. In Redux, we would use reducers to create a new state entry with the updated page property. However, this doesn’t update page in this.props.relay.variables. And we cannot use the setVariables function to update this data in our reducer, as this would violate the pure-function methodology that makes Redux clean and consistent. Not to mention that good programming practices would dictate that maintaining this data in two places is a mistake that can cause headaches down the line.

We found that one way to solve this problem was to use a slightly different way of wrapping the components when setting up the application, before we use the pattern above. We start with Redux’s Provider as the parent, to make sure that the Redux store is passed down the tree to other components. Just as we normally would have done with Redux, we stored our params in the state, and created actions and reducers to update these values.

entry.jsx

const Relay = require('react-relay');
const React = require('react');
const {render} = require('react-dom');
const {Provider} = require('react-redux');
const store = require('./ItemListStore');
const RootReduxContainer = require('./RootReduxContainer');

/*
store will contain our state, which in turn contains our params. For example:
{
    params: {
        page: 2
    }
}
*/
render(
    <Provider store={store}>
        <RootReduxContainer />
    </Provider>,
    document.getElementById('redux-relay-app')
);

Directly under the provider we created a Redux container (RootReduxContainer) that pulls these params from the state, storing them in currentParams. This container then wraps a simple React component (called RelayWrapperComponent), providing these params as a prop.

RootReduxContainer.jsx

const { connect } = require('react-redux');
const RelayWrapperComponent = require('./RelayWrapperComponent');

const mapStateToProps = (state) => {
    return {
        currentParams: Object.assign({}, state.params)
    };
};

module.exports = connect(mapStateToProps)(RelayWrapperComponent);

Finally, the React component renders our Renderer component. Now we have the parameters available as currentParams which can be passed to our Relay route, ItemRelayRoute, which we will talk about shortly. The container specified to the Renderer is just our Relay container from earlier, ItemRelayContainer. In the render function the same props (including the params) are passed to this component - now ItemRelayContainer and it’s descendants (which as we have seen are Redux components) will be able to modify the parameters based on user actions.

RelayWrapperComponent.jsx

const React = require('react');
const Relay = require('react-relay');
const ItemRelayContainer = require('./ItemRelayContainer');
const ItemRelayRoute = require('./ItemRelayRoute');
const store = require('../../store/ItemStore.es');
const UIActionNames = require('../../constants/UIActionNames.es');

const RelayWrapperComponent = ({currentParams}) => {
    return (
        <Relay.Renderer
            Container={ItemRelayContainer}
            queryConfig={new ItemRelayRoute(currentParams)}
            environment={Relay.Store}
            onReadyStateChange={({ready}) => {
                if (store.getState().ui.loading === ready) {
                    store.dispatch({
                        type: UIActionNames[ready ? 'LOADING_DONE' : 'LOADING']
                    });
                }
            }}
            render={({props}) => {
                if (props) {
                    return (<ItemRelayContainer {...props} />);
                } else {
                    return (<div>Loading...</div>);
                }
            }}
        />
    );
};

RelayWrapperComponent.propTypes = {
    currentParams: React.PropTypes.object
};

module.exports = RelayWrapperComponent;

As we mentioned above, the params were also passed to our route, ItemRelayRoute. The Relay route is the root for our GraphQL queries and mutations, defining which parts of our schema we are going to query. In the code below, the current params are passed in as the params argument, and the child component (which is ItemRelayContainer) is passed in as the Component argument. The params are then passed into the query fragment that is going to get run by this component. Note that we are querying using a root type called viewer (and also using that identifier for our fragment). It is a common practice to define a viewer as a root type in a GraphQL server, which users can then use to query different types of fields under it.]

ItemRelayRoute.js

const Relay = require('react-relay');

class ItemRelayRoute extends Relay.Route {}

ItemRelayRoute.queries = {
    viewer: (Component, params) => Relay.QL`
        query {
            viewer {
                ${Component.getFragment('viewer', params)}
            }
        }
    `
};

ItemRelayRoute.routeName = 'ItemRelayRoute';

module.exports = ItemRelayRoute;

The rest of the app under the Renderer component can now continue the structure from our first example. When an action occurs, Redux updates the params using reducers. Because our main Relay component is ultimately wrapped by a Provider component, changes in state will cause an updated set of params to be passed to the Renderer component, causing a data refresh. Once the data is refreshed, the subsequent Redux and React components can update as well. In essence, this allows us to forgo using props.relay.variables, or the setVariables function. The Relay components will still use the variables object for the GraphQL query, but our work of maintaining param data is now kept to one place, and cleanly maintained with pure functions. Finally, we have included the updated Relay component, using the passed in parameter.

Updated ItemRelayContainer.jsx

const React = require('react');
const Relay = require('react-relay');
const ItemReduxComponent = require('./ItemReduxComponent');

module.exports = Relay.createContainer(ItemReduxComponent, {
    fragments: {
        viewer: () => Relay.QL`
            fragment on Viewer {
                items(
                    page: $page
                ) {
                    id
                    status
                    photos {
                        webPath
                    }
                }
            }
        `
    }
});

Although this solution has quite a complicated nesting (first wrapping Relay with Redux and then later wrapping Redux with Relay), it has allowed us to update our apps to fetch data using Relay and to have the page refresh properly when new data is needed. At the same time we are able to keep our Redux code, allowing us to cleanly maintain our application state and handle events properly. Of course, web development technologies are ever changing. It is possible that there is a less-nested solution we have not thought of, or that may be coming in the future. As more people use Relay, it will be interesting to see how different developers use it for their specific needs.