Editor’s Note: The API used by this blog series is no longer available. As a result, you will not be able to run this application locally, but you can read the code and explanations to see how to build a robust real-world frontend app data layer.
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!