Redux and React

Background

Shortly after the release of React hooks, the community questioned whether React itself would replace Redux with its own Context API or Hooks libraries. The belief was so widespread that one of the maintainers had to declare Redux "not dead" in his blog post: Redux - Not Dead Yet.

Now that the dust has settled on Hooks for some time, we can conclude that Redux will not only survive, but thrive with the new Redux Toolkit. This was created by the same maintainer (Mark Erikson) mentioned above, and is now the "official recommended approach for writing Redux logic" according to the Redux docs.

The main reason for users wanting an alternative to Redux was the belief that it has an overly verbose and tedious setup, and requires additional packages. Redux Toolkit offers some exciting solutions to these issues, and we're going to explain those here in this post.

It includes several utility functions that simplify the most common Redux use cases, including store setup, defining reducers, immutable update logic, and even creating entire "slices" of state at once without writing any action creators or action types by hand. It also includes the most widely used Redux addons, like Redux Thunk for async logic and Reselect for writing selector functions, so that you can use them right away.

The New Redux Template for Create-React-App

In a tweet this February 18th, 2020, Mark Erikson announced the all new Redux template for the create-react-app boilerplate.

This is a huge milestone for state management in React apps. Bringing together the patterns of React hooks and introducing all new simplicity with Redux Slices, the new Redux boilerplate truly is an overhaul on previous Redux architecture and appears to make all previous Redux code obsolete.

Getting started with Redux and React

To create a React app with Redux, all you need to do is run the command:

npx create-react-app my-app --template redux

What this does is create a new app called my-app with the brand new Redux template baked in.

After running that command, make sure to cd my-app and then run yarn start to start the server, as you would if you were running a normal create-react-app project. See our webstorm tutorial if you're new to this.

You should now see the new React/Redux app launch in your browser:

Reviewing the new Redux file structure

Navigate to the src directory and you will find a surprisingly small amount of code compared to older Redux apps.

You will first see a features directory. The Redux team is now following a "domain-driven" file structure. This means organizing files by what "domain" or feature-set they belong to, rather than what "type" of file that is. If you've ever seen a Redux app before, you may have seen folders like "reducers", "actions" or "containers". These prior attempts at organization just didn't scale well. If you wanted to learn how a specific feature worked in your app, you'd need to look in many different folders.

So keeping all the files related to a "Counter" feature inside a features/counter becomes common sense.

But the Redux team took this a step further.

Redux Slices

Instead of creating separate files for reducers and actions, we now simply create a single file that contains a Slice. If you're new to Redux, we'll explain reducers and actions in just a second.

The important thing to know is that we're now combining the creation of reducers, actions and default state in a single method: createSlice.

Redux createSlice

Let's open counterSlice.js. As you can see, we're now importing from the new Redux toolkit package. This is where the magic happens.

import { createSlice } from '@reduxjs/toolkit';
export const slice = createSlice({
name: 'counter',
initialState: {
value: 0,
},
reducers: {
increment: state => {
// Redux Toolkit allows us to 'mutate' the state. It doesn't actually
// mutate the state because it uses the immer library, which detects
// changes to a "draft state" and produces a brand new immutable state
// based off those changes
state.value += 1;
},
decrement: state => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload.amount;
},
},
});

The createSlice method takes in an options object to configure your initial state and reducers for this specific slice.

Wait, what exactly is a slice? A slice you can think of is a top-level data attribute on your app which corresponds to a feature. For example, you might have features/listings and features/hosts slices if you're running an AirBnB site.

The name attribute here corresponds to the name of this slice. This does a lot, so keep this in mind.

Below the creation of the above Slice, we export some data:

export const selectCount = state => state.counter.value;
export const { increment, decrement, incrementByAmount } = slice.actions;
export default slice.reducer;

We'll talk about selectCount selector later.

Redux Slice Actions and Reducers

Next, we can see that we destructure some actions off of slice.actions. And the default export for this file, is the slice.reducer.

The biggest takeaway if you've seen previous Redux apps, is that the new Slice automatically names the actions for us based on the reducers names. Previously we would have to have a tedious file of action names that looked like: export const COUNTER_INCREMENT_VALUE, which was most likely the biggest turnoff about Redux.

The second thing you'll notice about these reducers is that the switch statement is gone. Instead, we have the action name as the key to the reducer function.

decrement: state => {
state.value -= 1;
}

New Immer State Mutations

From the redux toolkit docs,

Since the "lookup table" approach is popular, Redux Toolkit includes a createReducer function similar to the one shown in the Redux docs. However, our createReducer utility has some special "magic" that makes it even better. It uses the Immer library internally, which lets you write code that "mutates" some data, but actually applies the updates immutably. This makes it effectively impossible to accidentally mutate state in a reducer.

If you remember the old Redux code, you'd end up writing code that has a bunch of ...state spread operations, so that we never accidentally overwrite existing state data. Directly modifying the state object was a huge-no-no.

Until now.

Once again, quoting the docs:

This complex and painful code:

return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue
}
}
}
}

Can be simplified down to just:

updateValue(state, action) {
const {someId, someValue} = action.payload;
state.first.second[someId].fourth = someValue;
}

What's going on here? We're directly modifying the state. In old Redux, we're returning a brand new object, reconstructing it with our changes, careful not to modify state directly. We needed to return brand new objects, because if you returned the same object, the === comparator Redux uses to compare object references would fail to detect your new changes.

In our new Redux app, Immer is used internally to detect state changes and you can directly modify state all you want, making the reducers much cleaner.

Detour: React-redux explained: mapStateToProps is now useSelector

Let's discuss the selectCount selector method exported here. This will be used in the Counter.js inside the useSelector hook exported from react-redux. Confused? Remember that Redux can be used outside of React.

So, react-redux is the official bindings of React to Redux. From the react-redux docs:

React Redux is the official React binding for Redux. It lets your React components read data from a Redux store, and dispatch actions to the store to update data.

Previously, before hooks, we had mapStateToProps as a way to tell React what data from the Redux store we want. Now, we just export a simple pure function, which we then call like a regular React hook:

export function Counter() {
const count = useSelector(selectCount);
const dispatch = useDispatch();
const [incrementAmount, setIncrementAmount] = useState(2);

The count variable now is easily accessible from inside the Counter render method. We don't have to pull Redux data from a prop anymore, so as you can see, this is another huge syntax win for Redux.

So that's how you access Redux data to use in React functional component.

useDispatch

Now in our Counter.js, we import our actions:

import {
decrement,
increment,
incrementByAmount,
selectCount,
} from './counterSlice';

This is so when we call the useDispatch() hook in our render method, we access a dispatch method. We can then use the dispatch method to call one of our actions:

<button
className={styles.button}
onClick={() =>
dispatch(
incrementByAmount({ amount: Number(incrementAmount) || 0 })
)
}
>
Add Amount
</button>

Still stuck?

Weren't able to find what you need? Book a free call with a React School developer today.

๐Ÿ›ˆ React School creates templates and video courses for building beautiful apps with React. Download our free Material UI template.