One-directional code writing to help starters understand redux
TL;DR
Split logic into domains.
Each domain contains action-types, actions, reducers, selectors and services.
write from : services => action-types => actions =>reducers =>selectors => containers => components.
Repeat this a few times and break away from it.
The Challenge
When my colleagues and I started writing code using React 4 months ago, i thought redux was part of react, since everyone was using it alongside react and so it would be straight forward. It became evident, however, that many developers have their own interpretations of how to handle redux. After going through millions of articles here on Medium and other great sources, I created an easy way to help starters like myself understand it.
This guide is not the Bible truth, and I encourage any criticism, positive or otherwise. Just read through and see how you can apply it to your project.
Redux Domains
I encourage splitting the redux store into domains that represent modules of your applications. It makes the code more modular and readable to other developers.
Example:
Store:
Users:
actionTypes.js
actions.js
reducers.js
selectors.js
service.js
Posts:
actionTypes.js
actions.js
reducers.js
selectors.js
service.js
The Workflow
There is a lot of back and forth between files when writing the logic of the applications. This workflow is meant to help the developer only write once in each file, in the order above without having to go back.
This does not mean that you should not go back and forth, it only makes it simple when getting started. My colleagues and I found that after using this workflow 4–5 times, we were much more comfortable working on any file without fear of the breaking the code. I encourage anyone who uses this to also do the same.
Setup
You can use any boilerplate of your choice. create-react-app is a good boilerplate to use. You can also use mine
service.js
This file will make the API calls to your server. For this example, I’ll fetch posts from this site.
class JsonPlaceHolderService{
static getPosts(){
const url = 'https://jsonplaceholder.typicode.com/posts'
//define my request
const request = {
method: "GET",
};
//make the call and return a json object of the response
return fetch(url, request)
.then(response => {
return response.json()
})
.catch(error => {
throw( error)
})
}
}
export default JsonPlaceHolderService
actionTypes.js
In this file we define the nature that our actions. They describe what kind of actions we can dispatch.
export const POSTS_REQUESTED = 'posts.POSTS_REQUESTED'
export const POSTS_RECEIVED = 'posts.POSTS_RECEIVED'
actions.js
Action creators are called from the user interface and dispatch actions of a certain type as listed above.
import * as types from "./actionTypes"
import JsonPlaceHolderService from "./service"
export function fetchPosts(){
return function(dispatch, getState){
//dispatch an action to show the starte
dispatch({type: types.POSTS_REQUESTED})
return JsonPlaceHolderService.getPosts()
.then(posts =>{
dispatch({
type: types.POSTS_RECEIVED,
payload : posts
})
})
.catch(error =>{
throw(error)
})
}
}
reducer.js
The reducer will receive any action that is created and will switch based on the type of action defined in action types. In this case, it will add to an array of posts.
I have used seamless immutable to make the state immutable.
import * as types from "./actionTypes"
import Immutable from "seamless-immutable"
const initialState = Immutable({
posts: undefined,
postsIsFetched: false
})
export default function postsReducer(state=initialState, action={}){
switch (action.type) {
case types.POSTS_RECEIVED:
return state.merge({
posts: action.payload,
postsIsFetched: true
})
default:
return state
}
}
selectors.js
Selectors provide data from the store( passed as state ) to the user interface( Container). In this case, the selector only returns the actual state value, but you can do manipulations to the objects before feeding it to the containers e.g. sorting, filtering e.t.c. Note that the values are accessed through the state.reducer.variable
export function getPosts(state){
return state.postsReducer.posts
}
export function getPostStatus(state){
return state.postsReducer.postsIsFetched
}
Containers
Containers are components that are connected to the redux store and access store variables through the state. they use mapStateToProps(), mapDispatchToProps(), bindActionCreators() and connect() functions. We will name our container PostScreen.
The variables from the state will be passed as props into container hence access using this.props.variable
The container and components are not stored in a domain folder because, there are not part of Redux and one container can be connected to multiple domains (e.g. user, posts )
import { connect } from 'react-redux'
import React, { Component } from "react"
import { bindActionCreators } from "redux"
//import actions and selectors from the Store folder
import * as postActions from "../Store/Posts/actions"
import * as postSelectors from "../Store/Posts/selectors"
//import your component
import PostItem from "../Components/PostItem"
class PostsPage extends Component {
//when the component is mount, call the fetch post action
componentDidMount(){
this.props.postActions.fetchPosts()
}
//get alert from the
showPost(post){
alert(post.body)
}
render(){
return(
<div>
{
this.props.postsIsFetched ?(
<div>
{this.props.posts.map((post, i) => (
<PostItem key={i}
post={post}
clickAction={this.showPost}/>
))}
</div>
):(
<h3>loading</h3>
)
}
</div>
)
}
}
//get the posts and the fetch status from the post selector
const mapStateToProps = (state, ownProps) => {
return {
postsIsFetched: postSelectors.getPostStatus(state),
posts: postSelectors.getPosts(state)
}
}
//mapp all actions in the post store to prop: postActions
const mapDispatchToProps = (dispatch, ownProps) => {
return {
postActions: bindActionCreators(postActions, dispatch)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(PostsPage)
Component
The component is just a component that is not connected to the state (stateless components). It only receives its props from the container.
import React from 'react'
export const PostItem = (props) => {
return(
<div>
<span>{props.post.title}</span>
<button
onClick={() =>{props.clickAction(props.post)}}>
view details
</button>
</div>
)
}
export default PostItem
The bridge
We need to tie the store to the entire React app and we do this using the Provider component. This is not included in the workflow because it is only done once during the setup:
import { render } from "react-dom"
import React from "react"
import { Provider } from "react-redux"
import { combineReducers, createStore, applyMiddleware } from "redux"
import thunk from 'redux-thunk'
//import the post screen
import PostScreen from "Containers/PostScreen"
//import all your reducers from the store
import postsReducer from "./Store/Posts/reducers"
//
const rootReducer = combineReducers({
postsReducer
})
const store = createStore(rootReducer, applyMiddleware(thunk))
render(
<Provider store={store}>
<PostScreen/>
</Provider>,
document.getElementById('app')
);
References
The final application can be found here
There is also a boilerplate built around this here
This article is heavily based on a great article by Tal Kol. Please check it out here
Another great article to look out for is this here
Conclusion
There no magic towards mastering redux. We can only try to stomach the fails as many times as is required to learn. I hope this helps you get a more consistent understanding of how flow moves in a redux application.