How to Get Started with Redux for JavaScript State Management

 cloudsavvyit.com  09/16/2020 11:00:00 

Redux logo

Redux is a state management tool, built specifically for client-side JavaScript applications that depend heavily on complex data and external APIs, and provides great developer tools that make it easy to work with your data.

What Does Redux Do?

Simply put, Redux is a centralized data store. All of your application data is stored in one large object. The Redux Devtoolsmake this easy to visualize:

A visualized Redux data store

This state is immutable, which is a strange concept at first, but makes sense for a few reasons. If you want to modify the state, you have to send out anaction, which basically takes a few arguments, forms a payload, and sends it over to Redux. Redux passes the current state to areducer function, which modifies the existing state and returns a new state that replaces the current one and triggers a reload of the affected components. For example, you might have a reducer to add a new item to a list, or remove or edit one that already exists.

Doing it this way means you’ll never get any undefined behavior with your app-modifying state at will. Also, because there’s a record of each action, and what it changed, it enables time-travel debugging, where you can scroll your application state back to debug what happens with each action (much like a git history).

A record of each action

Redux can be used with any frontend framework, but it’s commonly used with React, and that’s what we’ll focus on here. Under the hood, Redux uses React’s Context API, which works similarly to Redux and is good for simple apps if you want to forego Redux altogether. However, Redux’s Devtools are fantastic when working with complex data, and it’s actually more optimized to prevent unnecessary rerenders.

If you’re using TypeScript, things are a lot more complicated to get Redux strictly typed. You’ll want to follow this guide instead, which uses typesafe-actionsto handle the actions and reducers in a type-friendly manner.

Structuring Your Project

First, you’ll want to lay out your folder structure. This is up to you and your team’s styling preferences, but there are basically two main patterns most Redux projects use. The first is simply splitting each type of file (action, reducer, middleware, side-effect) into its own folder, like so:

store/
  actions/
  reducers/
  sagas/
  middleware/
  index.js

This isn’t the best though, as you’ll often need both an action and reducer file for each feature you add. It’s better to merge the actions and reducers folders, and split them up by feature. This way, each action and corresponding reducer are in the same file. You

store/
  features/
    todo/
    etc/
  sagas/
  middleware/
  root-reducer.js
  root-action.js
  index.js

This cleans up the imports, as now you can import both the actions and reducers in the same statement using:

import { todosActions, todosReducer } from'store/features/todos'

It’s up to you whether you want to keep Redux code in its own folder (/storein the above examples), or integrate it into your app’s root src folder. If you’re already separating code per component, and are writing a lot of custom actions and reducers for each component, you might want to merge the /features/and /components/folders, and store JSX components alongside reducer code.

If you’re using Redux with TypeScript, you can add an additional file in each feature folder to define your types.

Installing and Configuring Redux

Install Redux and React-Redux from NPM:

npm install redux react-redux

You’ll also probably want redux-devtools:

npm install --save-dev redux-devtools

The first thing you’ll want to create is your store. Save this as /store/index.js

import { createStore } from 'redux'
import rootReducer from './root-reducer'

const store = createStore(rootReducer)

export default store;

Of course, your store will get more complicated than this as you add things like side-effect addons, middleware, and other utilities like connected-react-router, but this is all that’s required for now. This file takes the root reducer, and calls createStore()using it, which is exported for the app to use.

Next up, we’ll create a simple to-do list feature. You’ll probably want to start by defining the actions this feature requires, and the arguments that are passed to them. Create a /features/todos/folder, and save the following as types.js:

export const ADD = 'ADD_TODO'
export const DELETE = 'DELETE_TODO'
export const EDIT = 'EDIT_TODO'

This defines a few string constants for the action names. Regardless of the data you’re passing around, each action will have a typeproperty, which is a unique string that identifies the action.

You aren’t required to have a type file like this, as you can just type out the string name of the action, but it’s better for interoperability to do it this way. For example, you could have todos.ADDand reminders.ADDin the same app, which saves you the hassle of typing _TODOor _REMINDER every time you reference an action for that feature.

Next, save the following as /store/features/todos/actions.js:

import * as types from './types.js'

export const addTodo = text => ({ type: types.ADD, text })
export const deleteTodo = id => ({ type: types.DELETE, id })
export const editTodo = (id, text) => ({ type: types.EDIT, id, text })

This defines a few actions using the types from the string constants, laying out the arguments and payload creation for each one. These don’t have to be entirely static, as they are functions—one example that you might use is setting a runtime CUIDfor certain actions.

The most complicated bit of code, and where you’ll implement most of your business logic, is in the reducers. These can take many forms, but the most commonly used setup is with a switch statement that handles each case based on the action type. Save this as reducer.js:

import * as types from './types.js'

const initialState = [
  {
    text: 'Hello World',
    id: 0
  }
]

export default function todos(state = initialState, action) {
  switch (action.type) {
    case types.ADD:
      return [
        ...state,
        {
          id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
          text: action.text
        }
      ]    

    case types.DELETE:
      return state.filter(todo =>
        todo.id !== action.id
      )

    case types.EDIT:
      return state.map(todo =>
        todo.id === action.id ? { ...todo, text: action.text } : todo
      )

    default:
      return state
  }
}

The state is passed as an argument, and each case returns a modified version of the state. In this example, ADD_TODOappends a new item to the state (with a new ID each time), DELETE_TODOremoves all items with the given ID, and EDIT_TODOmaps and replaces the text for the item with the given ID.

The initial state should also be defined and passed to the reducer function as the default value for the state variable. Of course, this doesn’t define your entire Redux state structure, only the state.todossection.

These three files are usually separated in more complex apps, but if you want, you can also define them all in one file, just make sure you’re importing and exporting properly.

With that feature complete, let’s hook it up to Redux (and to our app). In /store/root-reducer.js, import the todosReducer (and any other feature reducer from the /features/folder), then pass it to combineReducers(), forming one top-level root reducer that is passed to the store. This is where you’ll set up the root state, making sure to keep each feature on its own branch.

import { combineReducers } from 'redux';

import todosReducer from './features/todos/reducer';

const rootReducer = combineReducers({
  todos: todosReducer
})

export default rootReducer

Using Redux In React

Of course, none of this is useful if it’s not connected to React. To do so, you’ll have to wrap your entire app in a Provider component. This makes sure that the necessary state and hooks are passed down to every component in your app.

In App.jsor index.js, wherever you have your root render function, wrap your app in a <Provider>, and pass it the store (imported from /store/index.js) as a prop:

importReactfrom'react';
importReactDOMfrom'react-dom';

//ReduxSetup
import{Provider}from'react-redux';
importstore,{history}from'./store';

ReactDOM.render(
<Providerstore={store}>
       <App/>
</Provider>
,document.getElementById('root'));

You are now free to use Redux in your components. The easiest method is with function components and hooks. For example, to dispatch an action, you will use the useDispatch()hook, which allows you to call actions directly, e.g.dispatch(todosActions.addTodo(text)).

The following container has an input connected to local React state, which is used to add a new todo to the state whenever a button is clicked:

importReact,{useState}from'react';

import'./Home.css';

import{TodoList}from'components'
import{todosActions}from'store/features/todos'
import{useDispatch}from'react-redux'

functionHome(){
constdispatch=useDispatch();
const[text,setText]=useState("");

functionhandleClick(){
dispatch(todosActions.addTodo(text));
setText("");
}

functionhandleChange(e:React.ChangeEvent<HTMLInputElement>){
setText(e.target.value);
}

return(
<divclassName="App">
<headerclassName="App-header">

<inputtype="text"value={text}onChange={handleChange}/>

<buttononClick={handleClick}>
AddNewTodo
</button>

<TodoList/>
</header>
</div>
);
}

exportdefaultHome;

Then, when you want to make use of the data stored in state, use the useSelectorhook. This takes a function that selects part of the state for use in the app. In this case, it sets the postvariable to the current list of todos. This is then used to render a new todo item for each entry in state.todos.

importReactfrom'react';
import{useSelector}from'store'

import{Container,List,ListItem,Title}from'./styles'

functionTodoList(){
constposts=useSelector(state=>state.todos)

return(
<Container>
<List>
{posts.map(({id,title})=>(
<ListItemkey={title}>
<Title>{title}:{id}</Title>
</ListItem>
))}
</List>
</Container>
);
}

exportdefaultTodoList;

You can actually create custom selector functionsto handle this for you, saved in the /features/folder much like actions and reducers.

Once you have everything set up and figured out, you might want to look into setting up Redux Devtools, setting up middleware like Redux Loggeror connected-react-router, or installing a side effect model such as Redux Sagas.

« Go back