I Bet I Could *Hook* You Into Redux Again

Starting on a new project that uses React as a front-end framework with Redux, I didn’t know what to expect. Coming from native iOS development, handling state was relatively straightforward: if you made the decision to maintain a global application state, you created a locked-down singleton class with static methods to call throughout the app, and you’re done!

Little did I know that for web development in vanilla React, with its virtual DOM and re-rendering, the application state had to be passed up and down component trees as props. Enter Redux: a platform-agnostic state management system that enables you to store and access app state in a systematic way.

And since this almost-greenfield project was started in 2019, it took advantage of React’s functional components and hooks in React and Redux. Quite the learning curve, indeed.

To note, hooks were added to React in version 16.8.0 and Redux in version 7.1.0.

Here’s what I’ve learned about React/Redux/hooks while on the project. 

The code examples below assume we’re building a superhero detail display, a web layout where superhero details are displayed in an expanding and collapsing detail view. When a superhero’s button is clicked, the details of that superhero are passed to the detail view to be displayed.

How does Redux handle state?

As I mentioned above, React is a front-end web framework. It can be paired with Redux, which is a state management system. But they don’t have to be paired at all; both React and Redux are technologies that are independent of each other.

 

A diagram that shows the flow of data in Redux

To use Redux, we need three moving parts: the action, the reducer, and the store. The action carries the payload that will become the new state. The action is dispatched and passed to the reducer. The reducer returns the new state, and that new state is held in the store and able to be accessed by any component that needs it, as we’ll see below.

The reducer is passed to the store when it is initialized, and that store is passed to the app through a high-level Provider component:

import { Provider } from ‘react-redux’;
import { createStore } from 'redux';
import { characterReducer } from '../src/redux/reducers';

const store = createStore(characterReducer);

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

State in React apps are represented as JavaScript objects, with keys and values to represent state data. When a state change needs to occur, it’s packaged in the form of an action. An action is a JavaScript object with one required string property: type. The ‘type’ property is a string constant that describes the state change that the action will perform.

Any other property values usually represent the change in state. A common pattern is to have a second property called ‘payload’ with new state object as a value. That’s the pattern we’re using here to keep things simple.

It’s a good practice to return the action in an action creator, which is a function that accepts a value that represents any state change and returns the action object. Using action creators make state changes easier to mock and test.

// the character state, in ../data/character.js
export const character = {
        name: 'wonder woman',
        image: {
            url: '/images/wonderwoman.jpg'
        },
        publisher: 'DC Comics'
};

// the action with its action creator, in ../src/action.js
const UPDATE_DETAIL = "UPDATE_DETAIL";

const updateAction = {
        type: UPDATE_DETAIL,
        payload: character
};

// the action returned in an action creator
export function actionCreator(character) {
        return updateAction;
}

// the action returned in an action creator

export function actionCreator(character) {
        return updateAction;
}

When a state change occurs, this action and the previous state are both passed to the reducer, which returns a new state based on the type of state change that is triggered. This is usually done through an if/else or switch statement that checks the ‘type’ property of the passed action and returns a new state based on that. This new state is what is held in the global store.

Notice that the reducer is “pure”: there are no side effects that change the state. The previous state is replaced by the new state, which is what makes the state immutable. 🎉

Since we are using Redux in the modern way, we employ the use of Redux hooks to control the flow of our state data.

What are hooks?

So, what are hooks? Hooks are just functions…really! These functions are called on every re-render, keeping the state immutable; the state is scrapped and created anew each time. What they take as arguments and return are usually up to the writer of the function (and indeed, you can write your own), but a common pattern is to return a variable and a function that modifies that variable (aka a “setter” in some object-oriented languages). 

React hooks do interesting things under the hood. There are some rules and some conventions, such as hook function names starting in “use,” but that’s all you need to know about hooks for this article.

What are the standard hooks used for Redux?

  • useDispatch()
    • Returns ‘dispatch’ method that accepts an action as an argument
    • The action dispatched (or “called”) by the dispatch method is also passed into the Redux store’s reducer
  • useSelector()
    • Returns state from the global store
    • At any given render, the state values returned reflect the latest new state returned by the Redux store’s reducer

There’s also useStore(), but this hook should rarely be used: The store returned by this hook will not automatically update its state as the store state changes, so neither will any component that uses that store.

Let’s hook these hooks into our flow!

With all the players and their roles are established, we can track the flow of state change in Redux.

To trigger a state change, an action is dispatched. A dispatch function takes the action as an argument. We’ll use a hook to return a dispatch function.

Without hooks, we would have to import the global store and call its dispatch method directly, like store.dispatch(). Instead of calling the dispatch method of our store directly, we can use the method that’s returned from the useDispatch() hook. That way, we don’t have to import our store, which is an optimization that we get for free: we’re not importing any part of the store that we don’t need.

Since we’re using an action creator, when we dispatch an action, it’s sufficient to pass this action creator to the dispatch function.

import { useDispatch } from 'react-redux';
Import { actionCreator } from ‘../action’

// React functional component that returns a button
function HeroButton(props) {

    // storing the dispatch function returned from the useDispatch() hook
    const dispatch = useDispatch();
    
    // triggering a state change with a button click, which calls the reducer in the store
    function handleOnClick() {
        dispatch(actionCreator(props.character));
    };
    
    return (
        <SomeButton onClick={handleOnClick}>Wonder Woman</SomeButton>
    );
}

Before the “Wonder Woman” SomeButton is clicked for the first time, the store contains an empty character state, which is our initial state. After that button is clicked, the character state object that we use to update the detail view is passed to the action creator, which returns an action with the character state object as the action’s payload. This action is dispatched, which triggers a state change, passing that action to the reducer. This reducer will return the new state to the store, ready to be retrieved for use in a component.

To read the state stored in the global store in order to, for example, pass it down as a prop, we can use the state returned by useSelector().

import { useSelector } from 'react-redux';
Import { character } from ‘../data/character’;

function App() {

    // useSelector to get state from store
    const characterState = useSelector(
            state => ({
                name: state.name,
                image: state.image,
                publisher: state.publisher
            })
    );
    
    return (
        <>
        <HeroButton hero={character} />
        
        // the new state from useSelector() passed as a prop
        // the state’s property values will be displayed in the detail view
        <SomeDetailView character={characterState} />
        </>
    );
}

This is a trivial example for simplicity’s sake, where there is one character state object being passed around. The change of state process would be the same even if there were an array of character objects as well.

To sum up the flow:

  1. Pass an action to the method returned by useDispatch() to trigger a state change.
    1. It’s best practice to return an action in an action creator.
  2. The action is passed to the reducer (in the global store), which evaluates the action type and returns the new state. The new state is kept in the global store.
  3. Use useSelector() to return the current state from the global store.

A diagram that shows the flow of data in Redux, along with where Redux hooks are used in the data flow

And we’re done! Redux is just one tool in the toolbox for handling global state in our web apps, and I hope we now have a better understanding of how to use that tool in a modern way — with hooks!

 

Britney Smith is a graduate of the iOS apprenticeship here at Detroit Labs. She has worked with iOS and web projects and refuses to pick sides. Her other interests include camping and binging Netflix while on her standing bike.