Making Redux as declarative as Relay/GraphQL (tutorial)

Before we begin, I would like to address some comments I encountered a few months ago, shortly after my first post. See Why I develop developer tools.

Overview

This is a relatively simple tutorial for building a blog application using React, Redux, Providers, and Replicators, and this is what’s covered:

  • Creating, reading, updating, and deleting resources (managed by Redux)
  • Queries w/ sorting, limiting, selecting only what you need, etc.
  • Routing
  • Users
  • Themes
  • Setting the document title from within components
  • URL redirection from within components
  • Status codes (404, etc.) from within components
  • Server rendering
  • And everything works regardless of JavaScript being enabled!

Want to skip to the outcome of this tutorial? See Bloggur.

Side note: I was planning on creating a video of all of the steps within this article, from start to finish, but if everything seems understandable to most people, then it may not be necessary. Please let me know if you think a video would be helpful!

Prerequisites

This tutorial is written with the assumption that you are familiar with:

Flowcharts

We all like flowcharts, right? Hopefully these will help with visualizing what exactly is going on with React, Redux, Providers, and Replication better than I could describe with plain text.

In its simplest form:

React, Redux, Providers, and Replication

And for plenty of flowcharts describing Redux, see reactjs/redux#653.

You can have any number of providers for any number of purposes. And you can have any number of the same type of provider if the provider’s key is a function. The key should be a function of the component’s props and return some unique string to identify the provider.

So for example, we’re going to build a blog throughout this article and create an entry provider to create, read, update, and delete blog entries. This provider will have action creators like createEntry, updateEntry, etc., and reducers to manage states with keys like entryId, entryContents, etc.

We’ll need a provider instance per entry, so we’ll use a function as the provider key, which will be as simple as:

const key = ({ props }) => `entryId=${props.entryId}`;

(This is actually done for you by provide-crud, which we’ll be using).

Now all you have to do to manage the state of a specific entry within a React component is declare entryId and updateEntry as propTypes:

import React, { PropTypes } from 'react';
const EntryEditor = ({ classes, entryId, updateEntry }) => (
// classes also comes from a provider!
<div className={classes.EntryEditor}>
<button onClick={() => updateEntry({ entryContents: 'Nice!' })}>
Nice button!
</button>
</div>
);
EntryEditor.propTypes = {
classes: PropTypes.object.isRequired,
entryId: PropTypes.string.isRequired,
updateEntry: PropTypes.func.isRequired
};
export default EntryEditor;

And then it’s just a matter of passing some entryId prop to the component.

This allows you to think of your application state and managing that state as any other import statement, but within components! And your providers and their configuration can be thought of as any other dependency, but without being tightly coupled to your components!

As for replication, there’s not much to it, really. All you do is attach a replication object (or array of objects) which specifies the reducerKeys you want replicated (saved/restored), along with the replicator to use, which acts as a “translator” to/from your database, 3rd party API, whatever. You can also specify which keys should be sent to the client by using clientStateKeys. And you can specify which reducerKeys should be queryable, which brings us to one last point before we continue.

With replicators, you can find particular states of anything within your data source(s) by using a special query prop, which performs the search and passes a result prop only if the query matches a provider (otherwise it’s passed down like any other prop). Queries are declarative, just like your provider-specific propTypes. You can even use a function to return a query based on the component’s props. So for example, if you want to find entries created by some specific user (which is also managed by a user provider):

import React, { PropTypes } from 'react';
const UserEntries = ({ classes, result }) => (
<div className={classes.UserEntries}>
// if entries found, it will be an array of entry states
{JSON.stringify(result)}
</div>
);
UserEntries.propTypes = {
classes: PropTypes.object.isRequired,
userId: PropTypes.string.isRequired,
query: PropTypes.any,
result: PropTypes.any
};
UserEntries.defaultProps = {
query: ({ props }) => ({ entryByUserId: props.userId }),
queryOptions: {
select: [
'entryId',
'entryName'
]
}
};
export default UserEntries;

I don’t know about you, but this is very appealing to me. I think it greatly simplifies the development of all kinds of applications.

Let’s put it to the test and build a simple blog application now.

Starting from boilerplate

Lumbur is a pretty nifty boilerplate + generator to help you get started. I’m not going to list everything it includes here since all of that can be found within its README.

1 - Download the Lumbur archive from GitHub.

2 - Create a directory called bloggur and extract the contents of the archive into it.

3 - In your terminal, change to the new directory.

4 - Run npm install. You’ll eventually be prompted by Lumburjack for some info about the application you’re building. This is what I entered:

package name (lumbur): bloggur
proper name (no spaces) (Lumbur): Bloggur
description (Built with Lumbur.): A simple blog application built with `react-redux-provide`. Demonstrates truly universal rendering with replication and queries.
version (1.0.0-alpha.1):
author (timbur):
owner (i.e., repo user name) (loggur):
url (https://github.com/loggur/lumbur): https://github.com/loggur/bloggur
development server port (3000):
production server https port (4433):
production server http port (8080):
redirect domain.com to www.domain.com (true):
secret key (ygvkq0fowxr2uik9):
ssl directory (./ssl/):
ssl key file (server.key):
ssl cert file (www_domain_com.crt):
ssl ca files (bundle.crt):

5 - We’re going to cheat a little bit here. I already have some themes ready for the app we’re about to build. You’re here for the JavaScript right? Not the CSS. Lumbur uses CSS modules by default, FYI.

Grab dark.css and replace src/themes/dark/dark.css.

Grab light.css and replace src/themes/light/light.css.

6 - Now run npm run start:dev in your terminal to start the development server.

7 - Assuming you chose the default development port (3000), open http://localhost:3000 in your browser.

Creating the entry provider

The boilerplate already includes the following providers:

  • provide-page — Provides page (document) generation, universal server rendering, and action handling via forms. Uses provide-router under the hood, which makes it slightly easier to use React Router within the providers paradigm. The page provider subscribes to the router’s history to ensure the page always has what it needs to render the app and/or perform actions correctly.
  • provide-theme — Provides the currently selected theme’s classes to components.
  • provide-user — Provides user creation and authentication. Actually just a composition of provide-crud and provide-authentication.
  • provide-id-gen — Provides ID generation to other providers, e.g., the user provider and the entry provider we’re about to create.

So for this particular app, all we need is a provider to represent each blog entry. This consists almost entirely of create, read, update, and delete operations, so we can use the provide-crud factory and add some functionality specific to entries.

The remainder of this tutorial is mostly code with comments in between. I’ve tried a few different ways to describe exactly how to perform each step, and modified diffs seem to be best. JavaScript highlighting would of course be nice, so I’ve included the whole files below each diff. (Video(s) coming soon, if there’s a demand.)

See the whole files at this commit:

Changing the user URL

See the whole files at this commit:

Creating the UserEntryList and EntryLink components

See the whole files at this commit:

Creating the EntryCreator component

See the whole files at this commit:

Creating the Entry component

See the whole files at this commit:

Showing the currently selected EntryLink

See the whole files at this diff:

Adding the edit button for the entry author

See the whole files at this commit:

Creating the EntryEditor

See the whole files at this commit:

Creating the LatestEntryList component

See the whole file at this commit:

Summary

This is what we did:

  • We began from scratch (sort of) using some boilerplate with a generator, which gave us a good starting point as it included basic functionality like page generation, routing, themes, user authentication, and a handful of libraries for building the app with React, Redux, and Providers.
  • We created a custom provider for creating, reading, updating, and deleting entries. We used the provide-crud provider factory to produce a handful of predictable Redux actions and reducers to be used within our React components. The factory also produced some sane defaults for replicating each entry’s state to any data source and enables declarative query functionality from within our React components so that we may search for entries and display them within the app.
  • We changed the default user profile URL.
  • We created a component to list user entries.
  • We created a component to create entries.
  • We created a component to display an entry using Markdown.
  • We updated the user entry list to show which entry is selected, if any.
  • We added an edit button to the entry, only for the entry author.
  • We created an entry editor, with the ability to delete the entry.
  • We created a component to list the latest 10 entries from all users and display it on the home page.

Overall, the entire process was extremely declarative, and I hope it was all relatively straightforward and easy for you to follow! A blog app is relatively simple, but I think it’s a good way to demonstrate certain concepts that you will almost certainly encounter in any real world application; e.g.:

  • Creating, reading, updating, and deleting resources (managed by Redux)
  • Queries w/ sorting, limiting, selecting only what you need, etc.
  • Routing
  • Users
  • Themes
  • Setting the document title from within components
  • URL redirection from within components
  • Status codes (404, etc.) from within components
  • Server rendering
  • And everything works regardless of JavaScript being enabled!

What’s next?

So far, this is mostly a proof of concept to show that building real world applications using React, Redux, and Providers is not only feasible, but relatively easy. Once you’re familiar with a handful of concepts, building an application becomes extremely declarative.

This also isn’t a framework. You aren’t locked in. Sure, these libraries work with each other incredibly well, but the logic within the providers themselves could be used anywhere, as most have no dependencies. And most of the React components aren’t dependent on anything but React.

I can’t imagine an application that could not be achieved using this set of libraries. Sure, some niche areas might need some extra help, but if you think about it, all you ever really need is the ability to store and retrieve states of arbitrary objects along with the ability to manipulate those states. In my opinion, the user interface should ideally be as declarative as possible, and in many cases, it should dictate what is and isn’t possible, which exactly what’s achieved here.

But maybe I just lack imagination. What do you think?

Moving forward, this is what I would like to address:

  • There are admittedly a few hacky things going on internally which should be refactored and optimized.
  • I’m sure there are unforeseen bugs, but we’ll get to them when we get to them.
  • We also need better error handling and extensive unit tests.
  • I would like to discuss a standard set of query options, so that we can instantly swap out one replicator for another - i.e., instantly switch to a different database and everything will continue to work perfectly.
  • After we’ve reached a standardized set of query options, implementing official replicators for popular databases like RethinkDB, MongoDB, QraphQL, various SQL DBs, etc., etc., should be pretty straightforward.
  • A websocket replicator should be fairly quick and easy. I just haven’t gotten around to it. Primus is probably the best choice for this.
  • At some point I would like to write another short tutorial to demonstrate the ability to disable server rendering and go 100% serverless for the same exact application by simply changing the client-side replicator to one for AWS Lambda, Firebase, or whatever. After that, I think my obsession for a perfect (well, almost perfect) application architecture will be satisfied. 😅😅😂😂

Please let me know if you have any questions, comments, or concerns!