Good Bye Redux, Global state using React Hooks and useReducer function

Global state lifts state and puts it outside React components. This helps in sharing state between components. Most React apps use Redux or Mobx for global state management. In this article, we will learn how to use the new React Hooks API and useReducer function for global state management.

Introduction

This article is yet another tutorial or lab. So, I encourage readers to try out this tutorial after reading it. For this tutorial, we shall do a simple Todo app.

Todo app
Todo app

Todo app displays a list of todo items. It has just two functions: add a new todo item and delete a todo item.

Using Parcel

I got to know about Parcel via a blog post from Jakob Lind. Parcel is similar to Webpack but helps create our React app with zero configuration. It has built-in support for React, JavaScript and CSS bundling.

To get started, first install Parcel as a global package.

yarn global add parcel-bundler

Initialise or create a new project as we normally do.

yarn init

Add react dependencies.

yarn add react@next react-dom@next

We are using the next version of React. At the time of this writing, react@next points to 16.7-alpha which has the React Hooks API. If you are trying out this lab, a few months from now, React Hooks might be accepted as a feature and released as part of the standard React package. In that case, you won’t need the @next tag while installing React.

Add index.html with the following code.

<html>
<body>
  <div id="root" />
  <script src="./index.js"></script>
</body>
</html>

Parcel requires a HTML page to run a server. In the above HTML page, we refer an index.js. So, create an index.js with the following code.

console.log('Hello world');

Run parcel from command line.

parcel index.html

The above command starts a server. When we open localhost:1234, we should see a blank page. If we check console output, we should see Hello world printed out.

Todo app with React Hooks

React Hooks is a new API which is right now in proposal stage. With previous versions of React, we can write function components. But those function components cannot have state or lifecycle methods. With the new Hooks API, a function component can have state and lifecycle methods via hooks. Each hook is a function which begins with use. For example, useState is a hook which allows a function component to have state.

We shall write our entire Todo app with only function components using the new Hooks API. Our Todo app consists of a Form and a List component. The Form component allows us to add a Todo item using a TextBox and a Button. Whereas the List component displays a Todo list. Each item has a checkbox and displays the text. When the user clicks on the checkbox, we delete the Todo item. So, let’s write some code.

We start with index.js. Write some code to mount the App component using react-dom.

import React from 'react';
import ReactDOM from 'react-dom';
import App from './src/App';

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

Next, create a src folder. Within the folder, create a new file App.js.

import React, { useState } from 'react';
import Form from './Form';
import List from './List';
import './app.css';

export default function App(props) {
    let [items, setItems] = useState([]);

    return (
        <div className="app">
            <Form />
            <List items={items} />
        </div>
    );
}

App component makes use of useState hook. When we call useState, we create a new state. We use array de-structuring to specify a variable (items) and a function (setItems). The setItems function sets the state for items whenever we want to.

Our App component uses a Form component and a List component. The List component is the one which uses the items state and displays it.

Create three new files: Form.js, List.js and app.css. In Form component, place the following code.

import React from 'react';

export default function Form(props) {
    return (
        <form>
            <input type="text" placeholder="Add Todo" />
            <button>Add</button>
        </form>
    );
}

Form component has a TextBox and a Button. It does nothing for now. Next, place some code in List component.

import React from 'react';

export default function List(props) {
    return (
        <div className="list">
            {props.items.map(item => (
                <ListItem key={item.id} text={item.text} />
            ))}
        </div>
    );
}

function ListItem(props) {
    return (
        <div className="list__item">
            <input type="checkbox" />
            <span>{props.text}</span>
        </div>
    );
}

List component displays a collection of items. Each item has a checkbox and displays the text. Since, we have no items in our state, the List component displays nothing now.

Add a new Todo

Open the Form component. We shall now make the TextBox a controlled component. Import useState hook.

import React, { useState } from 'react';

Create a new state value. Place the following code at the top of the Form function. 

export default function Form(props) {
    const [value, setValue] = useState('');

Modify the TextBox component as follows.

<input 
  type="text" 
  placeholder="Add Todo" 
  value={value} 
  onChange={handleChange}
/>

Add a new function which modifies the state.

function handleChange(e) {
    setValue(e.target.value);
}

With the above changes, we have made a controlled component. Next, we add an onClick handler to the Button to add a new Todo item.

<button onClick={handleAdd}>Add</button>

Add the handleAdd function. This function delegates to a callback function that we specify in onAdd prop.

function handleAdd(e) {
    e.preventDefault();
    props.onAdd(value);
}

Here is the Form component after all changes.

import React, { useState } from 'react';

export default function Form(props) {
    const [value, setValue] = useState('');

    function handleAdd(e) {
        e.preventDefault();
        props.onAdd(value);
    }

    function handleChange(e) {
        setValue(e.target.value);
    }

    return (
        <form>
            <input 
                type="text" 
                placeholder="Add Todo" 
                value={value} 
                onChange={handleChange}
            />
            <button onClick={handleAdd}>Add</button>
        </form>
    );
}

The real code for adding a new Todo item is available in the top-level App component. Open App.js and add an onAdd prop to the Form component.

<Form onAdd={handleAdd} />

Create a new function to add a new Todo item.

function handleAdd(text) {
    items = items.slice();
    items.push({
        id: i++,
        text
    });
    setItems(items);
}

Each item has id and text property. ID property is nothing but an auto-incremented number. We get the text for the Todo item from the TextBox in the Form component. Finally, we initialise the i (auto-incremented id) outside the App component.

let i = 1;

export default function App(props) {

With these changes in place, you should be able to add new Todo items and see them in the list.

Delete a Todo item

Since we are already in the App component, let’s continue to work on it. Add an onDelete prop to the List component.

<List items={items} onDelete={handleDelete} />

Define the handleDelete function which removes a Todo item.

function handleDelete(id) {
    const index = items.findIndex(item => item.id === id);
    if (index !== -1) {
        items = items.slice();
        items.splice(index, 1);
        setItems(items);
    }
}

We get the id of the Todo item to delete. After finding the index of the item in the list, we use the splice method in the Array object to delete the item. The App component after these changes looks like so.

import React, { useState } from 'react';
import Form from './Form';
import List from './List';
import './app.css';

let i = 1;

export default function App(props) {
    let [items, setItems] = useState([]);

    function handleAdd(text) {
        items = items.slice();
        items.push({
            id: i++,
            text
        });
        setItems(items);
    }

    function handleDelete(id) {
        const index = items.findIndex(item => item.id === id);
        if (index !== -1) {
            items = items.slice();
            items.splice(index, 1);
            setItems(items);
        }
    }

    return (
        <div className="app">
            <Form onAdd={handleAdd} />
            <List items={items} onDelete={handleDelete} />
        </div>
    );
}

Open List.js file. Here, we have two components – List and ListItem.  Modify the List component to pass the onDelete prop to the ListItem component.

export default function List(props) {
    return (
        <div className="list">
            {props.items.map(item => (
                <ListItem key={item.id} 
                    onDelete={props.onDelete.bind(null, item.id)} 
                    text={item.text} 
                />
            ))}
        </div>
    );
}

In the ListItem component, we are going to make a few tweaks to how we remove a Todo item. Instead of immediately removing an item, when the user checks the box next to the Todo item, we strike-through the text. After three seconds, we remove the item by calling the onDelete callback prop.

Define checked state within ListItem component.

function ListItem(props) {
    const [checked, setChecked] = useState(false);

Make the checkbox an uncontrolled component by providing only the onChange prop.

<input type="checkbox" onChange={handleCheckChange} />

In the handleCheckChange function, set the checked state.

function handleCheckChange(e) {
    setChecked(e.target.checked);
}

When the checked state is true, we have a new textDecoration style. And this is set to line-through.

const style = checked ? {
    textDecoration: 'line-through'
} : {};

Attach this style to the Todo text.

<span style={style}>{props.text}</span>

The screenshot below shows the strike-through when the user clicks the checkbox.

Strike-through text

What we want to do next is going to illustrate another hook named useEffect. This hook is triggered whenever the component renders or updates itself. By using the useEffect hook, we don’t have to write lifecycle methods which we do while writing class-based components. So, let us see how it works. 

Import the useEffect hook.

import React, { useState, useEffect } from 'react';

Use the hook as follows.

useEffect(() => {
    if (checked) {
        const timeoutHandle = setTimeout(props.onDelete, 3000);
        return () => {
            clearTimeout(timeoutHandle);
        };
    }
}, [checked])

The hook has two parameters: a function and an array. React calls this function whenever the component renders. Since component renders whenever state or prop changes, we can limit calling the function by passing an array of variables to watch for. In this case, we have the checked state in the array. So, only when the checked state changes, React calls the function.

Within the function, we call the setTimeout function to call the onDelete callback function after 3 seconds. This happens only when the user clicks the checkbox. However, if the user un-checks the Todo item, we want to cancel the timer. By returning a function from the useEffect hook, we accomplish this. React calls the returned function before calling the useEffect function again.

Let’s say, we pass an empty array as the second parameter to the useEffecthook. In that case, React calls the useEffect function when the component first renders. Later, when the component unmounts from the DOM, React calls the return function from the useEffect hook. In this way, we can mimic the componentDidMount and componentWillUnmount lifecycle methods.

The full code for List.js is shown below for completeness.

import React, { useState, useEffect } from 'react';

export default function List(props) {
    return (
        <div className="list">
            {props.items.map(item => (
                <ListItem key={item.id} 
                    onDelete={props.onDelete.bind(null, item.id)} 
                    text={item.text} 
                />
            ))}
        </div>
    );
}

function ListItem(props) {
    const [checked, setChecked] = useState(false);
    
    useEffect(() => {
        if (checked) {
            const timeoutHandle = setTimeout(props.onDelete, 3000);
            return () => {
                clearTimeout(timeoutHandle);
            };
        }
    }, [checked])

    function handleCheckChange(e) {
        setChecked(e.target.checked);
    }

    const style = checked ? {
        textDecoration: 'line-through'
    } : {};

    return (
        <div className="list__item">
            <input type="checkbox" onChange={handleCheckChange} />
            <span style={style}>{props.text}</span>
        </div>
    );
}

Global state with useReducer hook

We shall now do some refactoring to lift the items state into a separate reducer. So, what are reducers? If you have used Redux before, this is not new to you. But, if you have not used Redux before, reducer is a function that takes an old state and returns a new state.

Open the App component. Instead of useState hook, import the useReducer hook.

import React, { useReducer } from 'react';

 Replace the first line to make use of useReducer hook.

export default function App(props) {
    let [items, dispatch] = useReducer(todoReducer, []);

We have not defined todoReducer yet. But we will do it shortly. The useReducer function accepts two parameters – the reducer function and an initial state to pass to the reducer. Since our Todo list is an array, we pass an empty array. As the return array from the hook, we get a new state which we have defined as the items array and another function which is usually defined as dispatch. This dispatch function is very similar to the dispatch function we have in Redux. Calling the function dispatches an action object to the reducer. Let’s see how it works by rewriting the handleAdd and the handleDelete function in the App component.

function handleAdd(text) {
    dispatch({
        type: 'ADD_TODO',
        text
    });
}

function handleDelete(id) {
    dispatch({
        type: 'DELETE_TODO',
        id
    });
}

The new functions dispatch an action object to the reducer. Each action object has a type property. We will understand why this property is important when we write our reducer. For now, each action object has the type property and some arbitrary parameters for each action type. Add Todo action sends the Todo text. Whereas Delete Todo action sends the id of the item to delete.

Create a new file todoReducer.js. Place the following code.

let i = 1;

export default function(state, action) {
    switch(action.type) {
        case 'ADD_TODO': {
            const newState = state.slice();
            newState.push({
                id: i++,
                text: action.text
            });
            return newState;
        }
        case 'DELETE_TODO': {
            const index = state.findIndex(item => item.id === action.id);
            if (index !== -1) {
                const newState = state.slice();
                newState.splice(index, 1);
                return newState;
            }
            return state;
        }
        default:
            return state;
    }
}

Our reducer is a function that accepts the old state and returns a new state. It checks for the action type. And depending on what it is, it changes the state differently. So, for ADD_TODO action, we add a new Todo item to the list and return the new array. In DELETE_TODO action, we splice the item from the array and return it.

To get it all to work, import the todoReducer in App.js.

import todoReducer from './todoReducer';

How is useReducer hook different from Redux?

The new global state looks very much like Redux but with few differences. In Redux, we have a store composed of a hierarchy of reducers. We dispatch an action to the store which passes the action to all the reducers. In this way, the component does not need to know the reducer to which it has to dispatch the action object. In my opinion, this de-coupling is usually not very useful.

However, Redux has another useful concept called middleware. Middleware acts upon an action before the action reaches the store. Redux thunk is an example of middleware. With redux thunk, we can dispatch a function to the store. The thunk middleware will execute the function which in-turn will dispatch actual action objects to the store. Though we can write those thunk functions which can do multiple dispatches to the store, our code will suffer from being coupled to a specific reducer.

Finally, if you decide to go for only useReducer hook instead of Redux, you are most likely going to miss the redux-logger middleware. This middleware logs the actions dispatched to the store in the console.  It is very useful for debugging in development environments.

For small and medium projects, I will recommend to use the useReducer hook instead of Redux. But as the application grows in complexity, we probably need the de-coupling that Redux provides.

An undesirable side-effect of using Redux is having too many wrapper components. Maybe, Dan Abramov has plans of rewriting Redux (specifically react-redux) to use hooks instead of higher order components.

Summary

React Hooks is a big game-changer. With the new API, writing React code is much cleaner. All our code is just writing functions and more functions. A lot of online courses (and blog articles) may look obsolete with the “new vision for React”.

In this article, we created a Todo app with only function components and using the new Hook API. We used Parcel to bootstrap a React app. The app consists of a container App component with two child components – Form component and List component. With the help of useState hook, were able to add a Todo item and display it. Deleting a Todo item was done using both the useState hook and the useEffect hook. Finally, we did a refactor to remove state from the App component and used the useReducer hook to manage the global state within a reducer.

The entire code is available in a github repo.

The possibilities of using the Hook API is endless. For small apps, we have now the ability to use global state using the useReducer hook. This removes the wrapper components we observe while using Redux. But as a side-effect, there is a tight-coupling in our code because we dispatch the action object to a specific reducer instead of a store.

Related Posts

5 thoughts on “Good Bye Redux, Global state using React Hooks and useReducer function

  1. Why are u doing items = items.slice() ?

    function handleAdd(text) {
        items = items.slice();  // <---------
        items.push({
            id: i++,
            text
        });
        setItems(items);
    }
    
  2. One other advantage of redux is that the store has “all” the state. So middleware could serialize the state and restore the app to that state, for example on a crash for debugging/recovery, or for global undo/redo.

  3. Title is wrong, useReducer is NOT global state. You’d have to pass local state to props manually.

    Also this piece of code:

    Never do that. You’re binding a function on every render of each item… if you need to bind do it in constructor or use arrow functions.

Leave a Reply

Your email address will not be published.