image credit: Steve Halama

Turning Requirements into React/Redux Code

last updated: Dec 7th, 2017

Working with new technology can be so frustrating!

You've got a simple goal you're trying to achieve -- or at least it should be simple. It was simple in ${otherTechnology}. Why do I feel STUCK? I'm not getting anywhere!!

Even decades of experience count for little when faced with a paradigm shift as shifty as React/Redux.

A question came up on Reactiflux from someone who was having trouble getting from here to there.

I have a container that fetches some posts. I want to show a loading spinner if I'm fetching posts for the first time, otherwise I'd like to show the old data, which is in the store and refetch silently in the background.

I'm going to show you how to translate these requirements into React/Redux code.

Before we begin, it should be noted that there are many ways to 'skin a cat', as the saying goes. This article features technology I've learned and had success with on multiple projects. It's certainly not the only way to write React/Redux.

Breaking it Down

1. I have a container that fetches some posts.

First of all, it sounds like there is already some code in place. The poster didn't link to any code so I'm going to proceed as if we're starting from a blank slate.

We're going to need two React components. The first is the container component that manages posts and the second is a component to display an individual post.

The poster may be doing API calls directly from the component. I want to steer you away from doing that. Although there are many ways to achieve the same outcome, separating application logic from view logic is considered 'Best Practice'.

Since we're going to separate out the application logic, we'll need some Redux infrastructure (actions, action creators, a reducer, and some selectors) to manage the loading of posts.

2. I want to show a loading spinner if I'm fetching posts for the first time

Sounds reasonable. We'll need a third component for the spinner.

We'll also need some way to determine if we're fetching for the first time or some subsequent time. Since we're going to be driving the API calls from Redux infrastructure, it makes sense that the Redux infrastructure owns that knowledge. And we'll pass it as props to the container component.

3. otherwise I'd like to show the old data, which is in the store

The container component will conditionally show the spinner or the posts, which is easy to accomplish in React.

4. and refetch silently in the background.

We'll get 'silently' for free because we're putting the fetching code in Redux infrastructure -- in a thunk to be exact, but a saga is perfectly acceptable too.

And we'll assume the background refreshes happen periodically as long as the container component is mounted. We can achieve this by registering a timer in the component.

And Now Some Code

Let's start with a basic React container that will show the posts. This isn't the fully finished code... we'll build up to that.

I'm a strong believer in the simplicity of React's stateless functional components. These are just functions that take props as parameters and return TSX markup.

// PostList.tsximport * as React from 'react';import { PostItem } from './PostItem';import { PostId, PostMap } from 'types';export const PostList = ({ postIds, postMap }: Props) =>    <div className="post-list">        { postIds.map(postId =>            <PostItem                key={ postId }                post={ postMap[postId] }            />        ) }    </div>;type Props = {    postIds: PostId[];    postMap: PostMap | null;};

Here are the types I'm using throughout the code.

// types.tsexport type PostId = string;export type Post = {    postId: PostId;    title: string;    content: string;    // ...};export type PostMap = {    [postId: string]: Post;}

Redux Infrastructure

// actions.tsimport { PostMap } from 'types';import { apiGet } from './api';export const fetchPostsRequest = () => ({    type: 'FETCH_POSTS_REQUEST',});export const fetchPostsSuccess = (posts: PostMap) => ({    type: 'FETCH_POSTS_SUCCESS',    payload: posts,});export const fetchPostsFailure = (err: Error) => ({    type: 'FETCH_POSTS_SUCCESS',    error: true,    payload: err,});export const fetchPosts = () => async (dispatch: Function) => {    dispatch(fetchPostsRequest());    try {        // Assuming posts are returned as a PostMap object rather than a Post[] array        const posts = await apiGet('/api/posts');        dispatch(fetchPostsSuccess(posts));    }    catch (err) {        dispatch(fetchPostsFailure(err));    }};
// reducers.tstype PostsState = {    loading: boolean;    data: PostMap | null;    error: Error | null;};export type State = {    posts: PostsState;    // ...};const initialPostsState: PostsState = {    loading: false,    data: null,    error: null,};export const postsReducer = (state: PostsState = initialPostsState, action: Action) => {    switch (action.type) {        case 'FETCH_POSTS_REQUEST':            return {                ...state,                loading: true,            };        case 'FETCH_POSTS_SUCCESS':            return {                ...state,                data: {                    ...state.data,                    ...action.payload,                },                loading: false,                error: null,            };        case 'FETCH_POSTS_FAILURE':            return {                ...state,                loading: false,                error: action.payload,            };        default:            return state;    }}
// selectors.tsimport { State } from 'reducers';import { createSelector } from 'reselect';export const getPostMap = (state: State) => state.posts.data;// Only recalculates when the posts.data object changesexport const getPostIds =    createSelector(        getPostMap,        postMap => postMap && Object.keys(postMap) || []    );export const isLoadingPosts = (state: State) => state.posts.loading;

Connect the Container to Redux

Now that we've got the Redux infrastructure set up, let's go ahead and connect our component to it.

// PostList.tsximport * as React from 'react';import { compose, lifecycle } from 'recompose';import { connect } from 'react-redux';import { State } from 'reducers';import { PostItem } from './PostItem';import { Post } from 'types';import { fetchPosts } from 'actions';import { getPostIds, getPostMap, isLoadingPosts } from 'selectors';export const RawPostList = ({ postIds, postMap }: StateProps & DispProps) =>    <div className="post-list">        { postIds.map(postId =>            <PostItem                key={ postId }                post={ postMap[postId] }            />        ) }    </div>;type StateProps = {    postIds: PostId[];    postMap: PostMap | null;};type DispProps = {    onFetchPosts: () => any;};const mapStateToProps = (state: State) => ({    postIds: getPostIds(state),    postMap: getPostMap(state),});const mapDispatchToProps = (dispatch: Function) => ({    onFetchPosts: () => dispatch(fetchPosts()),});export const PostList = compose(    connect<StateProps, DispProps, {}>(mapStateToProps, mapDispatchToProps),    lifecycle({        componentDidMount() {            this.props.onFetchPosts();        },    }),)(RawPostList);

Using Recompose is instrumental in remaining purely stateless and functional. It's quite powerful but some of its features are complex and take some mind-bending to comprehend. So I'll understand if you want to stick to React classes.

Just know that you don't have to use React classes.

Conditionally Show the Spinner

So far we've set up the store state such that postMaps is null to start. We can use this to decide if we're in a first-load situation. And when postMaps is non-null we know we've already loaded the data at least once.

// PostList.tsx// ...import { Spinner } from './Spinner';export const RawPostList = ({ postIds, postMap }: StateProps & DispProps) =>    <div className="post-list">        { postMap            ? postIds.map(postId =>                <PostItem                    key={ postId }                    post={ postMap[postId] }                />            )            : <Spinner/>        }    </div>;type StateProps = {    // ...   isLoading: boolean;};// ...const mapStateToProps = (state: State) => ({    // ...    isLoading: isLoadingPosts(state),});// ...export const PostList = compose(    connect<StateProps, DispProps, {}>(mapStateToProps, mapDispatchToProps),    lifecycle({        componentDidMount() {            const { postMap, onFetchPosts, isLoading } = this.props;            // Only start a fetch on the first mount            if (!postMap && !isLoading) {                onFetchPosts();            }        },    }),)(RawPostList);

Refetch the Posts in the Background

We'll use Recompose again to introduce some 'local state', wrapped around our stateless functional component. The state will hold the interval ID and we'll also add another lifecycle method, componentWillUnmount, where we can cancel our timer.

// PostList.tsx// ...import { compose, lifecycle, withState } from 'recompose';// ...type RecomposeProps = {    timerId: NodeJS.Timer;    setTimerId: (id: NodeJS.Timer) => any;};export const PostList = compose(    connect<StateProps, DispProps, {}>(mapStateToProps, mapDispatchToProps),    withState('timerId', 'setTimerId', 0),    lifecycle({        componentDidMount() {            const { postMap, onFetchPosts, isLoading, setTimerId } =                this.props as StateProps & DispProps & RecomposeProps;            // Only start a fetch on the first mount            if (!postMap && !isLoading) {                onFetchPosts();                setTimerId(setInterval(onFetchPosts, 5 * 60 * 1000));            }        },        componentWillUnmount() {            const { timerId } = this.props as RecomposeProps;            clearInterval(timerId);        },    }),)(RawPostList);

And that's it

Here's a summary of the technology used in this article:

Did you find this article helpful? Want more?
Sign up on my mailing list so I can share more with you.

Are you curious about trying TypeScript on a React/Redux project?
Follow my quick guide: Starting a React/Redux Project with TypeScript

And check out my latest articles on TypeScript and Redux

Level up Your React + Redux + TypeScript

with articles, tutorials, sample code, and Q&A.