Implement a Reusable Reducer Logic
I met a mentee who was working on a project that required him to display two music playlists of different genre on the same page. He didn't want to write two separate reducers for this component as the only difference was the playlist id. He reached out to me but I was unable to help him. I have never had any reason to create a reusable reducer. Although the mentee had access to the Reusing Reducer Logic Examples document, he was still unable to implement these examples in his project. I decided to write this tutorial to help other folks with the same need.
In this tutorial, I will share two implementations of Reusing Reducer Logic Examples with you. I will share them in a step-by-step approach on how to integrate this with your React project.
The first implementation will have a single key in the store with multiple branches while the second will have multiple keys in the store using the same reducer logic.
// First implementation store structure
{
posts:{
a_posts:{ items:[], isFetching: false},
b_posts:{ items:[], isFetching: false}
...
}
}
// Second implementation store structure
{
a_posts:{ items:[], isFetching: false},
b_posts:{ items:[], isFetching: false}
...
}
Audience
This tutorial assumes you are familiar with the concept of Redux. If that's not the case for you, read this Introduction to Redux and Getting Started with React Redux: An Intro first, then come back here to learn how to implement a reusable reducer logic.
What we are building
We will be creating a half-baked blog application that displays posts based on category. We are only implementing posts listing as it's the only functionality that requires a reusable reducer logic.
Environment Configuration
You should have Node installed before proceeding with this tutorial.
You can clone the repo, which is the final outcome of this tutorial, or you can follow it step by step as we iron things out.
Running cloned repo
If you cloned the repo, open your terminal and navigate to the project directory. Install dependencies and start the server. Follow the steps below:
cd react-redux-reusable-reducer
npm install
npm start
The project will compile, start a development server on http://localhost:3000/, and open your browser pointing to that address. You can check out to v2
branch to see the second implementation.
Project skeleton
To get started, we need to initialize a react project skeleton using create-react-app module. If you don't already have create-react-app, just run npm install -g create-react-app
in your terminal to install it globally. Next, we need to generate our React app boilerplate. Run the following in your terminal:
create-react-app react-redux-reusable-reducer
cd react-redux-reusable-reducer/
npm start
Open http://localhost:3000
to see your initial project skeleton.
Install Dependencies
We need to install some project dependencies. Feel free to take out isomorphic-fetch
if you are not calling external API with fetch
. Otherwise, run the following in your terminal:
npm install react-redux redux-thunk isomorphic-fetch --save
Actions
We will start off with the actions for our blog application. Take a look at the actions explanation if you don't already know what actions are.
Under the src
directory, create another directory called posts
. Inside the posts
directory, create a actions.js
file with the following content:
src/actions.js
// import fetch from 'isomorphic-fetch';
export const REQUEST_POSTS = "FETCH_POSTS";
export const RECEIVE_POSTS = "RECEIVE_POSTS";
let sampleResponse = [
{"title":"test title 1","id":1976235410884491574,"category":"a"},
{"title":"test title 2","id":3510942875414458836,"category":"a"},
{"title":"test title 3","id":6263450610539110790,"category":"b"},
{"title":"test title 4","id":2015796113853353331,"category":"b"}
]
export function requestPosts(name) {
return {
type: REQUEST_POSTS,
name
}
}
export function receivePosts(payload, name) {
return {
type: RECEIVE_POSTS,
name,
items: payload
}
}
export function fetchPosts(query, name) {
return dispatch => {
dispatch(requestPosts(name))
return new Promise(function(resolve, reject) {
setTimeout(function() {
let result = sampleResponse.filter(function(item){
return item.category === query.cat
})
resolve(result);
}, 1000);
}).then(payload => dispatch(receivePosts(payload, name)));
/*
// You can use this to call an external api endpoint
return fetch("http://example.com/api?cat=" + query.cat)
.then(function(response){
return response.json()
})
.then(payload => dispatch(receivePosts(payload, name)))
*/
}
}
If you examine the code above, you will notice that all the actions have a name
parameter โ this will be used to track the component that's dispatching an action.
Note: You do not need to call this parameter name
; you can call it whatever you want.
Reducers
It's time to write the reducer logic, which handles how the application's state changes in response to user actions. Read about Reducers if you are wondering what they are and how they work.
Inside posts
directory, create a file called reducers.js
with the following contents:
src/reducers.js (v1 implementation)
import {
REQUEST_POSTS, RECEIVE_POSTS
} from './actions'
let singleState = {items: [], isFetching: false};
let initialState = {
a_posts : singleState,
b_posts : singleState,
}
export function posts(state = initialState, action) {
let data = initialState;
switch (action.type) {
case REQUEST_POSTS:
data[action.name].isFetching = true;
return Object.assign({}, state, data)
case RECEIVE_POSTS:
data[action.name].items = action.items
data[action.name].isFetching = false;
return Object.assign({}, state, data)
default:
return state
}
}
In the reducer logic above, the initial state of each component will be stored in their own key.
let initialState = {
a_posts : singleState,
b_posts : singleState,
c_posts : singleState,
...
}
You can decide to add more to it as shown above. Also, note that the key name is the same name we dispatched in the actions. The new state is computed and returned into the corresponding key based on the name dispatched by the actions.
Now, let's take a look at the second implementation.
src/reducers.js (v2 implementation)
import {
REQUEST_POSTS, RECEIVE_POSTS
} from './actions'
let initialState = {items: [], isFetching: false};
export function postWithName(name) {
return function posts(state = initialState, action) {
if(name !== action.name)
return state;
switch (action.type) {
case REQUEST_POSTS:
return Object.assign({}, state, {isFetching: true})
case RECEIVE_POSTS:
return Object.assign({}, state, {isFetching: false, items: action.items})
default:
return state
}
}
}
The postWithName
function returns the inner function (the actual reducer logic) and the name
parameter is used to compare the name in the action dispatched. If the name is not equal, the current state is returned. See other implementations here.
Store
The store holds all application state. Read more about Store here if you don't already know.
We will implement the store logic in the entry file index.js
. Open your current index.js
and replace it with the following code:
src/index.js (v1 implementation)
import React from 'react';
import thunkMiddleware from 'redux-thunk';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import { posts } from './posts/reducers';
import App from './App';
function storeWrapper(state = {}, action) {
return {
posts: posts(state.posts, action)
}
}
let store = createStore(
storeWrapper,
applyMiddleware(
thunkMiddleware
)
)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
Nothing special here, only one key posts
. This will hold other keys as specified in the reducer logic, each key with it own state. Let's take a look at v2โs implementation.
src/index.js (v2 implementation)
import React from 'react';
import thunkMiddleware from 'redux-thunk';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import { postWithName } from './posts/reducers';
import App from './App';
function storeWrapper(state = {}, action) {
return {
a_posts: postWithName('a_posts')(state.a_posts, action),
b_posts: postWithName('b_posts')(state.a_posts, action)
}
}
let store = createStore(
storeWrapper,
applyMiddleware(
thunkMiddleware
)
)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
The only difference here is in the storeWrapper
function. The postWithName
function will return the actual reducer logic. I guess you probably figured out why we used double parenthesis ๐
return {
a_posts: postWithName('a_posts')(state.a_posts, action),
b_posts: postWithName('b_posts')(state.a_posts, action)
}
The first parenthesis calls the postWithName
function with a name for the component, which will be able to modify the state, while the second parenthesis calls the returned reducer logic with the current state and action.
Note: It's not compulsory that the key name will be the same name passed to postWithName
function.
Display Components
We are finished with the hard part. Let's create a component that will display the posts. Create a directory under the posts directory we created earlier โ this new directory will be called components
. Paste the following code into a file called posts.js
in the newly created directory.
src/posts/components/post.js
import React, { PropTypes, Component } from 'react'
export default class Posts extends Component {
render() {
return (
<ul>
{this.props.posts.map((post, i) =>
<li key={i}>{post.title} ({post.category}) </li>
)}
</ul>
)
}
}
Posts.propTypes = {
posts: PropTypes.array.isRequired
}
Display Container
The container will serve as a data source for our display component. This article explains it well. Create another directory containers
inside posts directory and add the following code into PostList.js
.
src/posts/containers/PostList.js (v1 implementation)
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import { fetchPosts } from '../actions'
import Posts from '../components/posts'
class PostContainer extends Component {
componentDidMount() {
const { dispatch} = this.props
dispatch(fetchPosts({cat:this.props.cat}, this.props.name))
}
render() {
const { posts, isFetching } = this.props
return (
<div>
{isFetching &&
<div>
Loading...
</div>
}
{posts.length > 0 &&
<div style={{ opacity: isFetching ? 0.5 : 1 }}>
<Posts posts={posts} />
</div>
}
</div>
)
}
}
PostContainer.propTypes = {
posts: PropTypes.array.isRequired,
cat: PropTypes.string.isRequired,
isFetching: PropTypes.bool.isRequired,
name: PropTypes.string.isRequired,
dispatch: PropTypes.func.isRequired
}
function mapStateToProps(state, props) {
return {
posts: state.posts[props.name].items,
isFetching: state.posts[props.name].isFetching,
}
}
export default connect(mapStateToProps)(PostContainer)
src/posts/containers/PostList.js (v2 implementation)
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import { fetchPosts } from '../actions'
import Posts from '../components/posts'
class PostContainer extends Component {
componentDidMount() {
const { dispatch} = this.props
dispatch(fetchPosts({cat:this.props.cat}, this.props.name))
}
render() {
const { posts, isFetching } = this.props
return (
<div>
{isFetching &&
<div>
Loading...
</div>
}
{posts.length > 0 &&
<div style={{ opacity: isFetching ? 0.5 : 1 }}>
<Posts posts={posts} />
</div>
}
</div>
)
}
}
PostContainer.propTypes = {
posts: PropTypes.array.isRequired,
cat: PropTypes.string.isRequired,
isFetching: PropTypes.bool.isRequired,
name: PropTypes.string.isRequired,
dispatch: PropTypes.func.isRequired
}
function mapStateToProps(state, props) {
return {
posts: state[props.name].items,
isFetching: state[props.name].isFetching,
}
}
export default connect(mapStateToProps)(PostContainer)
The difference in the two versions of code above is the mapStateToProps
functions. If you take a closer look at the returned object, you will notice that the first implementation receives values from state.posts[props.name]
while the second receives a value from state[props.name]
.
Final Step
It's time to connect the final piece of our blog posts application and, finally, test the result. In the root folder, replace App.js
contents with the following code:
src/App.js
import React from 'react'
import PostList from './posts/containers/PostList'
const App = () => (
<div>
<h3>Posts - Category A</h3>
<PostList cat="a" name="a_posts"/>
<h3>Posts - Category B</h3>
<PostList cat="b" name="b_posts" />
</div>
)
export default App
In the code above, the PostList
is used twice with two props cat
and name
. cat
is the value to use in filtering the incoming posts while name
is used to identify the component dispatched actions.
If you still have your server running, you should see the result in your browser. You can run npm start
in your terminal from the project root folder if your server is not up.
Result Screenshot
Final Note
I hope this tutorial has been helpful, You can fork the repo and feel free to contribute if you have other implementations.