Upcoming and OnDemand Webinars View full list

React Data Layer – Part 8: Where to Go From Here

Josh Justice

This post is the final part of an 8-part series going in-depth into how to build a robust real-world frontend app data layer. See the previous parts here:

In this series we’ve managed to build out a pretty robust data layer. It should provide you with the knowledge and patterns to build out great always-online or cached-read apps.

But when it comes to cached-writes, notice that we’ve only handled the case where we’re adding new records. We haven’t gotten into updates or deletes, because that’s a whole additional level of complexity. At that point, you can’t necessarily just keep a queue of records–you’re really keeping a queue of operations. Also, when data can be edited, you can run into conflicts where two conflicting operations are made at the same time. Resolving conflicts isn’t easy; you have to either present users with a UI to resolve the conflicts, or use an algorithm to resolve the conflicts, or use a special data type called a Conflict-Free Replicated Data Type (CRDT) that’s designed not to have conflicts. To learn more about these options, check out a conference talk by Jonthan Martin.

At that point, you’re starting to leave the realm of building software for your business, and you’re starting to build a substantial data synchronization system. That’s the point where it can be best to reach out for an off-the-shelf system. Here are a few options.

Orbit

Orbit.js is a framework for declaratively synchronizing data between sources, including in-memory, local storage, and REST web serviced. By “declarative” I mean that instead of having to implement each of the features we’ve done in this tutorial, you can simply configure each of them. It’s maintained by one of the primary editors of the JSON:API specification, so it’s closely aligned with the spec. Orbit represents data internally in JSON:API format, and it has built-in support for connecting with JSON:API servers. There is a third-party react-orbitjs library that provides React bindings, although it has even less adoption. Still, it provides a high degree of sophistication in declaratively setting up data flows in a JSON:API context. If you want to access data from a REST server offline, and you don’t want to integrate with an expensive commercial solution, Orbit is probably your best bet.

Here’s an example of the declarative configuration that replicates what we’ve built in this tutorial:

import { Schema } from '@orbit/data';
import Store from '@orbit/store';
import JSONAPISource from '@orbit/jsonapi';
import IndexedDBSource from '@orbit/indexeddb';
import Coordinator, { RequestStrategy, SyncStrategy } from '@orbit/coordinator';

const schemaDefinition = {
  models: {
    game: {
      attributes: {
        title: { type: 'string' },
      },
    },
  },
};
const schema = new Schema(schemaDefinition);
const store = new Store({ schema });

const remote = new JSONAPISource({
  schema,
  name: 'remote',
  host: 'https://sandboxapi.bignerdranch.com',
});

const backup = new IndexedDBSource({
  schema,
  name: 'backup',
  namespace: 'videogames',
});

const coordinator = new Coordinator({
  sources: [store, remote, backup],
});

// Query the remote server whenever the store is queried
coordinator.addStrategy(new RequestStrategy({
  source: 'store',
  on: 'beforeQuery',
  target: 'remote',
  action: 'pull',
  blocking: true,
}));
// Update the remote server whenever the store is updated
coordinator.addStrategy(new RequestStrategy({
  source: 'store',
  on: 'beforeUpdate',
  target: 'remote',
  action: 'push',
  blocking: false,
}));
// Sync all changes received from the remote server to the store
coordinator.addStrategy(new SyncStrategy({
  source: 'remote',
  target: 'store',
  blocking: false,
}));

// Back up data to IndexedDB
coordinator.addStrategy(new SyncStrategy({
  source: 'store',
  target: 'backup',
  blocking: false,
}));

// Restore data from IndexedDB upon launch
const restore = backup.pull((q) => q.findRecords())
  .then((transform) => store.sync(transform))
  .then(() => coordinator.activate());

export const restoreBackup = () => restore;

export default store;

And here’s the container component that makes the data and update operation available to the GameList:

import React, { Component } from 'react';
import GameList from './GameList';
import { withData } from 'react-orbitjs';
import { restoreBackup } from 'orbitStore';

class GameListContainer extends Component {
  async componentDidMount() {
    const { queryStore } = this.props;
    await restoreBackup();
    queryStore((q) => q.findRecords('game'));
  }

  handleAddGame = (newGameTitle) => {
    const { updateStore } = this.props;
    const game = {
      type: 'game',
      attributes: {
        title: newGameTitle,
      },
    };
    updateStore((t) => t.addRecord(game));
  }

  render() {
    const { games } = this.props;
    return <GameList games={games} addGame={this.handleAddGame} />;
  }
}

const mapRecordsToProps = {
  games: (q) => q.findRecords('game'),
};

export default withData(mapRecordsToProps)(GameListContainer);

GraphQL with Subscriptions

GraphQL is a query language that has gained a ton of momentum in the frontend and native development world in the last few years. Some of its strongest features include statically-typed data declarations, easy querying of exactly the data you need to avoid over-fetching and under-fetching, and subscriptions for push updates to data.

One of the most popular client libraries for GraphQL is Apollo Client, due to its excellent integration with frontend frameworks including React. Using GraphQL requires setting up a GraphQL server, but its architecture is well set-up to wrap existing REST services in GraphQL. If you still want to maintain control of your data stores but want to be using the platform for which there is the most community momentum, GraphQL is a good fit.

Here’s what the container component for the GameList looks like when loading the data via the Apollo Client:

import React, { Component } from 'react';
import { Query } from 'react-apollo';
import gql from 'graphql-tag';
import GameList from './GameList';

const GAME_QUERY = gql`
  {
    allGames {
      id
      title
    }
  }
`;

export default class GameListContainer extends Component {
  render() {
    return <Query query={GAME_QUERY}>
      {({ loading, error, data }) => {
        if (loading) return <p>Loading...</p>;
        if (error) return <p>Error.</p>;
        return <GameList games={data.allGames} />;
      }}
    </Query>;
  }
}

And here’s what the AddGameForm looks like to persist a game. (Note that the local cache of games is not automatically updated, so more work would be needed to get the new game to show up right away.)

import React, { Component } from 'react';
import {
  Button,
  Col,
  Input,
  Row,
} from 'react-materialize';
import { Mutation } from 'react-apollo'
import gql from 'graphql-tag'

const ADD_GAME_MUTATION = gql`
  mutation PostMutation($title: String!) {
    createGame(title: $title) {
      id
      title
    }
  }
`;

export default class AddGameForm extends Component {
  state = { newGameTitle: '' }

  handleChangeText = (event) => {
    this.setState({ newGameTitle: event.target.value });
  }

  handleAddGame = (postMutation) => (event) => {
    event.preventDefault();
    postMutation();
    this.setState({ newGameTitle: '' });
  }

  render() {
    const { newGameTitle } = this.state;
    return (
      <Mutation mutation={ADD_GAME_MUTATION} variables=>
        {(postMutation) => (
          <form onSubmit={this.handleAddGame(postMutation)}>
            <Row>
              <Input
                label="New Game Title"
                type="text"
                value={newGameTitle}
                onChange={this.handleChangeText}
                s={12} m={10} l={11}
              />
              <Col s={12} m={2} l={1}>
                <Button>Add</Button>
              </Col>
            </Row>
          </form>
        )}
      </Mutation>
    );
  }
}

Firebase

Firebase is a popular real-time database that’s very easy to get started with. It handles synchronization and real-time updates transparently. The Firebase JavaScript SDK works well directly with React without any custom binding library; a good introduction to using Firebase with React is on the readme for a previous and now-deprecated React/Firebase library. One downside, however, is that it’s a proprietary Google system, so your data doesn’t reside on your own servers. If realtime data is a must, though, Firebase may be your best bet.

Here’s what a Firebase container component looks like that provides a games and addGame prop to the GameList component. With this little code we already have reactivity, offline support, and even real-time push.

import React, { Component } from 'react';
import firebase from 'firebase/app';
import 'firebase/database';
import generateUuid from 'uuid/v4';
import GameList from './GameList';

export default class GameListContainer extends Component {
  state = {
    games: {},
  };

  componentDidMount() {
    firebase
      .database()
      .ref('/games')
      .on('value', (snapshot) => this.setState({ games: snapshot.val() }));
  }

  addGame = (title) => {
    const uuid = generateUuid();
    firebase
      .database()
      .ref(`/games/${uuid}`)
      .set({ title });
  }

  render() {
    const { games } = this.state;

    return <GameList games={games} addGame={this.addGame} />;
  }
}

So Long, and Thanks For All the Actions

Thanks for following along with us in this guide. You should now be thoroughly equipped to build out robust data layers in React or any other client app. Give these patterns a try, see what works for your application, find new patterns, and let us and the community know about them!

Not Happy with Your Current App, or Digital Product?

Submit your event

Let's Discuss Your Project

Let's Discuss Your Project