Redux Step by Step: A Simple and Robust Workflow for Real Life Apps

68,860 reads

no trees, just rocks

Redux has become one of the most popular Flux implementations for managing data flow in React apps. Reading about Redux, though, often causes a sensory overload where you can’t see the forest for the trees. Presented below is a straightforward and opinionated workflow for implementing real life apps with Redux. It is shown by example with a step by step implementation walkthrough of an actual app. It attempts to apply the principles behind Redux in a practical way and detail the thought process behind every decision.

An opinionated approach for idiomatic Redux

Redux has become more than just a library, it’s an entire ecosystem. One of the reasons behind its popularity is its ability to accommodate different writing styles and many different flavors. If I’m looking for asynchronous actions should I use thunks? or maybe promises? or sagas?

There isn’t one right answer to which flavor is “best”. And there isn’t one right way to use Redux. Having said that, too much choice is overwhelming. I want to present an opinionated flavor that I personally like. It’s robust, can deal with complicated real life scenarios — and most of all — it’s simple.

So let’s build an app!

We need a real life example to walk through. As long as we’re being opinionated, the most interesting place on the Internet is Reddit. Let’s make an app that shows the most interesting posts there.

On the first screen, we’ll ask the user for 3 topics they’re interested in. We’ll pull the list of topics from Reddit’s list of default front page subreddits.

After the user makes a choice, we’ll show the list of posts from each of these 3 topics in a filterable list — all topics or just one of the 3. When the user clicks on a post in the list, we’ll show its contents.

Setup

Since we use React for the web (we might add React Native in a future post), our starting point will be Create React App, the official starter kit. We’ll also npm install redux, react-redux and redux-thunk. The result should be something like this.

To get the boilerplate out of the way, let’s also quickly initialize the Redux store and hook up thunk middleware in index.js:

The Flux circle of life in a Redux app

One of the main things often missing from Redux tutorials is the grand picture and where Redux fits in. Redux is an implementation of the Flux architecture — a pattern for passing data around in a React app.

Under classic Flux, app state is held within stores. Dispatched actions cause this state to change, afterwhich the views that listen to these state changes will re-render themselves accordingly:

Flux simplifies life by making data flow in a single direction. This reduces the spaghetti effect as the codebase grows and becomes more complex.

One of the difficulties with understanding Redux is the plethora of unintuitive terms like reducers, selectors and thunks. It’s easier to see where they all fit in by placing them on the Flux diagram. These are simply the technical names of the various Redux constructs that implement the different parts of the cycle:

As you’ve probably noticed, other terms from the Redux ecosystem like middlewares and sagas, are absent. This is intentional as they won’t play a significant role in our workflow.

Project directory structure

We will organize our code according to the following top-level directory structure under /src :

/src/components

“Dumb” React components that have no knowledge of Redux

“Dumb” React components that have no knowledge of Redux /src/containers

“Smart” React components that are connected to our Redux store

“Smart” React components that are connected to our Redux store /src/services

Abstraction facades for external API (like backend servers)

Abstraction facades for external API (like backend servers) /src/store

All Redux-specific code goes here, including all business-logic of our app

The store directory is organized by domain, each containing:

/src/store/{domain}/reducer.js

Reducer as a default export with all selectors as named exports

Reducer as a default export with all selectors as named exports /src/store/{domain}/actions.js

All the domain action handlers (thunks and plain object creators)

A state-first approach

Our app has two screens, we’re going to start with the first and let the user choose exactly 3 topics. We can begin implementing any point of the Flux cycle, but I’ve discovered it’s usually easiest for me to start with the State.

So what app state does our topics screen need?

We’ll need to hold the list of topics retrieved from the server. We’ll also need to hold the ID’s of the topics chosen by the user so far (max 3). It will be nice to hold them in the order selected, so in case we already have 3 and another is chosen, we can simply drop the oldest one.

How will we structure this app state? There’s a list of actionable tips in my previous post — “Avoiding Accidental Complexity When Structuring Your App State”. Following the tips, this would be an appropriate structure:

The topic URL will serve as a unique ID.

Where will we hold this state? In Redux, the reducer is the construct that holds state and updates it. We will organize our code by domains, so the natural place for this reducer will be: /src/store/topics/reducer.js

There’s some boilerplate to create a reducer, you can see it here. Notice that in order to enforce immutability of our state (as required by Redux), I’ve chosen to use an immutability library called seamless-immutable.

Our first scenario

After modeling state, I like to take a user scenario and implement it from start to finish. In our case let’s create our topics screen and display some topics as soon as it shows up. This component will be connected to our reducer, meaning it’s a “smart” component that’s aware of Redux. We’ll place it in /src/containers/TopicsScreen.js

There’s some boilerplate to create a connected component, you can see it here. Let’s also display it as the content of our App component. Now, when everything is set up, we can fetch some topics.

Rule: Smart components are not allowed to have any logic except dispatching actions.

The scenario starts on the view’s componentDidMount . Since can’t run logic directly on the view, we will dispatch an action that will fetch the topics. This action is asynchronous of course, so it will be a thunk:

In order to abstract the Reddit server API, we’ll create a new service that does the actual network fetch. Its methods will be asynchronous so we can await for the response. In general, I love the async await API so much that my code hasn’t seen a direct use of promises in a long time.

The service returns an array , but our state structure stores the topics in a map . The action body is a good place to do the conversion. In order to actually store the data in the state, we must invoke our reducer by dispatching a standard plain object action — TOPICS_FETCHED .

The full source for this stage is available here.

A few words about services

Services are used to abstract external API — in many cases server API like the one provided by Reddit. The benefit of this abstraction layer is that API’s change and we want to decouple our code as much as possible from them. If in the future Reddit decides to rename endpoints or change field names, we can hopefully contain the impact on our app to the service alone.

Rule: Services must be completely stateless.

This is a tricky rule in our methodology. Imagine what would happen if our Reddit API required login. We might be tempted to hold this login state in the service by instantiating it with the login details.

This isn’t allowed in our methodology because all app state must be contained in the store. Holding state in a service would be a state leak. The acceptable approach in this case would be to provide every service function with login information as argument and hold the login state in one of our reducers.

Implementing the service is fairly straightforward, you can see it here.

Completing the scenario — reducer and view

The plain object action TOPICS_FETCHED arrives at our reducer and contains the freshly fetched topicsByUrl as parameter. Our reducer doesn’t need to do much except save this data on the state:

Notice the usage of seamless-immutable to make this immutable change explicit and straightforward. Immutability libraries are of course optional, I prefer their syntactic sugar to object spread tricks.

After the state updates, our view needs to re-render. This means the view needs to listen on the part of the state it cares about. This is done with mapStateToProps:

I decided that our view will render the list of topics using a separate ListView component that takes a rowsById map and a rowsIdArray (inspired by React Native). I’m using mapStateToProps to prepare these two props in TopicsScreen (they will later be passed on directly to the ListView ). The two props can be derived from our state. Notice something interesting, I don’t access the state directly..

Rule: Smart components should always access state through selectors.

Selectors are one of the most important constructs in Redux that people tend to overlook. A selector is a pure function that takes the global state as argument and returns some transformation over it. Selectors are tightly coupled to reducers and are located inside reducer.js . They allow us to perform a few calculations on data before it’s being consumed by the view. In our methodology, we take this idea even further. Every time anyone needs to access part of the state (like in mapStatetoProps ), they need to go through a selector.

Why? The idea is to encapsulate the internal structure of the app state and hide it from views. Imagine that we decide later on to change the internal state structure. We wouldn’t want to go over all the views in our app and refactor them. Passing through a selector will allow us to confine the refactoring to the reducer only.

This is what does our topics/reducer.js look like:

The entire current state of our app, including ListView , can be seen here.

A few words about “dumb” components

ListView is a good example of a “dumb” component. It is not connected to the store nor aware of Redux at all. Unlike the “smart” connected components that are located in /src/containers , these components are located in /src/components

“Dumb” components receive data from their parents through props and may hold local component state. Assume you’re implementing a TextInput component from scratch. The blinking caret position is an excellent example for local component state that should not find its way into your global app state.

So when do we need to move from a “smart” component to a “dumb” one?

Rule: Minimize view logic in smart components by extracting it into dumb components.

If you look at the implementation of ListView, you will see it contains view logic like iterating over rows. We want to avoid having this logic in our smart TopicsScreen component. This keeps our smart components as wirings only. Another benefit is that the ListView logic is now reusable.

Next scenario — multiple topic selection

We’ve completed our first scenario. Let’s move on to the next one — having the user select exactly 3 topics from the list.

Our scenario starts with the user clicking on one of the topics. This event is handled by TopicsScreen but since this smart component cannot contain any business logic, we’ll dispatch a new action — selectTopic . This action will also be a thunk, placed in topics/actions.js . As you can see, almost every action we export (to be dispatched by views) is a thunk. We usually only dispatch plain object actions from within a thunk in order to update the reducer state.

An interesting aspect about this thunk is that it needs to access the state. Notice how we keep the rule that every state access goes through a selector even here (although some may claim it’s going a bit too far).

We’ll have to update the reducer to handle the TOPICS_SELECTED action and store the new selected topics. There’s an interesting question whether selectTopic needs to be a thunk at all. Alternatively, we could make selectTopic a plain object action and move this business logic to the reducer itself. This is a valid strategy. Personally I prefer to keep the business logic in thunks.

Once the state updates, we need to propagate the topic selection back to our view. This means adding the selected topics in mapStateToProps . Since the view needs to query whether every rowId is selected or not, it is more convenient to pass this data to the view as a map. Since the data has to go through a selector anyways, this will be a great place to do the transformation.

After implementing the above, and refactoring the background color change due to row selection into a new dumb component — ListRow — our app looks like this.

A few words about business logic

One of the goals of a good methodology is achieving proper separation between views and business logic. Where was our business logic implemented so far?

All business logic was implemented under Redux in the /src/store directory. Most of it was inside thunks in actions.js and some it was inside selectors in reducer.js . This is actually an official rule:

Rule: Place all business logic inside action handlers (thunks), selectors and reducers.

Navigating to the next screen — the posts list

When we have more than one screen we need a way to navigate. This is usually achieved using a navigation component like react-router. I want to deliberately avoid using a router in order to keep our example simple. Opinionated external dependencies like routers tend to draw attention away from the conceptual discussion of methodology.

Instead, let’s add a state variable , selectionFinalized , telling us whether the user completed topic selection or not. Once the user selects 3 topics, we will display a button that once clicked — will finalize selection and move to the next screen. Clicking the button will dispatch an action that sets this state variable directly.

This is all fairly similar to what we’ve been doing so far, the only interesting part is knowing when to display the button (as soon as at least 3 topics are chosen). We may be tempted to add another state variable for this purpose, but this variable can actually be derived from data we already have in the state now. This means we should implement this business logic as a selector:

The full implementation of the above is available here. In order to do the actual screen switch, we’ll need to change App into a connected component and have it listen on selectionFinalized in its mapStateToProps . The full implementation is available here.

The posts screen — once again state first

Since we’re now well experienced in the methodology, we can run through implementing the second screen a bit faster. This new screen deals with a new domain — posts. In order to make our app modular as possible, we’ll give this domain a separate reducer and separate app state.

Reminder — the screen’s purpose is to display a list of posts that can be filtered according to topic. The user can click on a post in the list and see its content. Following our structuring tips, this would work:

And our new posts reducer is born.

First scenario — showing the posts list without filter

As usual, when our state is modeled, we move to a simple user scenario and implement it from start to finish. Let’s start with showing the full post list without any filter applied.

We need a new smart container to show the posts, we’ll call it PostsScreen and have it dispatch a new action called fetchPosts when it mounts. The action will be a thunk under our new domain in posts/actions.js

This is very similar to what we did before, the implementation is here.

At the end of the thunk we dispatch the plain action POSTS_FETCHED that carries the posts to the reducer. We’ll have to modify our reducer to store the data. In order to show the list in PostsScreen , we need to hook up its mapStateToProps to a selector providing this part of the state. We can then display the list by reusing our ListView component.

Nothing new as well, the implementation is here.

Next scenario — filter the post list

This scenario starts with showing the user the available filters. We can pull this data from the topics reducer state using an existing selector. When a filter is changed, we will dispatch an action that will change it directly in the posts reducer.

The interesting part is applying the filter to the post list. In our app state we currently hold all postsById and the currentFilter . We don’t want to hold the filtered result in the app state as well because it can be derived from them. Business logic for deriving data runs in selectors right before arriving at the view in mapStateToProps . Our selector therefore will be:

The full implementation for this stage is available here.

Last scenario — showing post details

This scenario is actually the simplest one yet. We have an app state variable holding the currentPostId . All we have to do is update it when the user clicks on a post in the list by dispatching an action. PostsScreen needs this state variable in order to show the post details, which means we’ll need a selector to drive it in mapStateToProps .

Take a look at the detailed implementation here.

And we’re done!

This also wraps up the implementation of our entire example app. The full source code of the app is available on GitHub:

https://github.com/wix/react-dataflow-example

Summary of our opinionated workflow rules

App state is a first class citizen, structure it like an in-memory database.

Smart components are not allowed to have any logic except dispatching actions.

Smart components should always access state through selectors.

Minimize view logic in smart components by extracting it into dumb components.

Place all business logic inside action handlers (thunks), selectors and reducers.

Services must be completely stateless.

Remember, Redux offers a lot of room for personal style. There are many alternate workflows with different sets of rules. Some good friends of mine come to mind who prefer redux-promise-middleware instead of thunks and like to place all of the business logic inside reducers only.

If you want to share a different methodology that works for you, feel free to PR your own implementation for the above project and we’ll provide it as a branch for comparison.

Tags