How to get started with Redux for JavaScript state management – CloudSavvy IT





Redux logo

Redux is a state management tool, designed specifically for client-side JavaScript applications that rely heavily on complex data and external APIs, and provides excellent development tools that make it easier to work with your data.

What does Redux do?

Simply put, Redux is a centralized data store. All of your app data is stored in one large object. Redux Devtools facilitate visualization:

A visualized Redux data store

This state is immutable, which is a strange concept at first, but makes sense for several reasons. If you want to change the status, you must send a action, which basically takes a few arguments, forms a payload, and sends it to Redux. Redux changes the current state to one reducer function, which changes the existing state and returns a new state that replaces the current state and triggers a reload of the affected components. For example, you can have a reducer to add a new item to a list, or remove or edit an item that already exists.

Doing it this way means you’ll never get undefined behavior with your app changing state at will. Also, because there is a record of every action and what it has changed, it enables debugging over time, where you can scroll through your app’s state to debug what is happening with each action. (much like a git history).

A record of each action

Redux can be used with any front-end framework, but it’s commonly used with React, and that’s what we’ll be focusing on here. Under the hood, Redux uses React’s Context API, which works the same as Redux and is good for simple apps if you want to forgo Redux altogether. However, Redux’s Devtools are fantastic when working with complex data, and they’re actually more optimized to avoid unnecessary renderings.

If you are using TypeScript things are much more complicated for Redux to be strictly typed. Instead, you’ll want to follow this guide, which uses typesafe-actions to manage actions and reducers in a user-friendly way.

Structure your project

First, you’ll want to set up your folder structure. It depends on you and your team’s style preferences, but there are basically two main templates that most Redux projects use. The first is to simply split each type of file (action, reducer, middleware, side effect) into its own folder, like this:

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

It’s not the best though, as you’ll often need an action file and a shrink file for each feature you add. It is better to merge the actions and reducers folders and divide them by functionality. In this way, each action and the 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 imports, as you can now import both actions and reducers in the same statement using:

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

It’s up to you to decide if you want to keep the Redux code in its own folder (/store in the examples above), or embed it in the src root folder of your application. If you already separate the code by component and write many custom actions and reducers for each component, you might want to merge the /features/ and /components/ folders and store the JSX components with the reducer code.

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

Installation and configuration of Redux

Install Redux and React-Redux from NPM:

npm install redux react-redux

You will probably also want redux-devtools:

npm install --save-dev redux-devtools

The first thing you will 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 that as you add things like side effect add-ons, middleware, and other utilities like connected-react-router, but that is all that is required for now. This file takes the root reducer and calls createStore() use it, which is exported for the application to use.

Next, we’re going to create a simple to-do list feature. You will probably want to start by defining the actions required by this feature and the arguments passed to them. To create a /features/todos/ folder and save the following items as types.js:

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

This defines some string constants for action names. Whatever data you pass around, every action will have a type property, which is a unique string that identifies the action.

You don’t have to have a file of type like this because you can just type the string name of the action, but it’s better for interoperability to do it that way. For example, you might have todos.ADD and reminders.ADD in the same app, so you don’t have to type _TODO Where _REMINDER whenever you reference an action for that feature.

Then 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 of the string constants, exposing the arguments, and creating a payload for each. These do not need to be entirely static, as they are functions. An example that you can use is setting an execution CUID for certain actions.

The most complicated piece of code, and where you’ll implement most of your business logic, is in reducers. These can take many forms, but the most common configuration used is to use a switch statement that handles each case depending on the type of action. 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_TODO adds a new element to the state (with a new ID each time), DELETE_TODO deletes all items with the given id, and EDIT_TODO maps and replaces the element’s text with the given ID.

The initial state must also be defined and passed to the reduction function as the default value for the state variable. Of course, this does not define your entire Redux state structure, only the state.todos section.

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

Once this feature is complete, let’s connect it to Redux (and our app). In /store/root-reducer.js, import the todosReducer (and any other functionality reducer from the /features/ folder), then send it to combineReducers(), forming a top level root reducer which is passed to the store. This is where you will configure 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 this, you will need to wrap your entire application in a provider component. This ensures that the necessary state and hooks are passed to every component in your application.

In App.js Where index.js, wherever you have your root renderer, wrap your app in a <Provider>, and send it to the store (imported from /store/index.js) as an accessory:

import React from 'react';
import ReactDOM from 'react-dom';

// Redux Setup
import  Provider  from 'react-redux';
import store,  history  from './store';

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

You are now free to use Redux in your components. The easiest way is to use function components and square brackets. For example, to send an action, you would use the useDispatch() hook, which allows you to directly call actions, for example dispatch(todosActions.addTodo(text)).

The following container has an entry connected to the local React state, which is used to add a new task to the state each time a button is clicked:

import React,  useState  from 'react';

import './Home.css';

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

function Home() 
  const dispatch = useDispatch();
  const [text, setText] = useState("");

  function handleClick() 
    dispatch(todosActions.addTodo(text));
    setText("");
  

  function handleChange(e: React.ChangeEvent<HTMLInputElement>) 
    setText(e.target.value);
  

  return (
    <div className="App">
      <header className="App-header">

        <input type="text" value=text onChange=handleChange />

        <button onClick=handleClick>
          Add New Todo
        </button>

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


export default Home;

Then when you want to use the data stored in the report, use the useSelector to hang up. It takes a function which selects part of the report to use in the application. In this case, it defines the post variable to the current task list. This is then used to make a new to-do item for each entry in state.todos.

import React from 'react';
import  useSelector  from 'store'

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

function TodoList() 
  const posts = useSelector(state => state.todos)

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


export default TodoList;

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

Once you’ve got everything set up and figured out, you might want to consider setting up Redux Devtools, setting up middleware like Redux Logger or connected-react-router, or installing a side-effect model such as Redux Sagas.




Leave a Comment

x