Oscar's Dev Blog


2022-06-27

Learn Redux the right way: With Redux Toolkit


Introduction

Redux! The word which you've been hearing over and over again. What is it? Why is it needed? Maybe you've glanced at the daunting Redux docs and thought "Nah, I'll learn that some other day". Well, today is that day and I guarantee that it will be easier than expected.

Many existing Redux tutorials are outdated, but this guide will show you the latest, recommended way to implement the framework into your app using redux-toolkit. We will go over it in 5 simple steps. It is recommended that you follow along as we learn how to set up, read and write data to Redux. The rest, you can only learn through further experimenting.

What is Redux?

Redux is a global state management system for your entire web/react app. Imagine a state which is shared by every component which can be read and updated at any level, on any page. No callback functions.

1. Set-up

Lets get started. For this tutorial we be using NextJs and TypeScript. Start by initialising a project. npx create-next-app@latest --ts

In the root of your project, install the dependencies react-redux and @reduxjs/toolkit. npm i react-redux @reduxjs/toolkit

If you would rather just start playing around with the tutorial code now, the repository can be found on my github.

2. Create a slice

Slices are the functions which define how a global state is managed. In a slice, we define the initial state and also the reducers which define how the data is manipulated. Create the file src/reducers/FooReducer.tsx containing the code, below.

// fooReducer.tsx

import { createSlice } from '@reduxjs/toolkit';

const initialValue = { name: "Nigel", age: 63 };

export const fooSlice = createSlice({
    name: 'foo',
    initialState: {value: initialValue},
    reducers: {
        changeAll: (state, action) => {
            state.value = action.payload;
        },
        agePlusOne: (state, action) => {
            state.value.age += 1;
        }
    }
})

export const { changeAll, agePlusOne } = fooSlice.actions;
export default fooSlice.reducer;

It looks like there is a lot going on, but it will become clearer. Trust me. Let's focus on what matters here. We have an initialValue which defines the initial value of an object containing a 'name' and 'age' value. Under reducers we have two special functions which show how the data can be manipulated. We may add as many of these reducer functions as we need.

The function changeAll takes in an object with new key values e.g. {name: 'Bob', age: 44} and replaces the current key values.

The function getOlder takes no parameters, and increases the age value by 1.

3. Set up Provider

In order for the Redux state to be synchronised across the app, we must nest everything inside a <Provider/> component. Copy the code below into pages/_app.tsx.

// _app.tsx
import type { AppProps } from 'next/app'

import { configureStore } from '@reduxjs/toolkit';
import { Provider } from 'react-redux';
import fooReducer from '../src/reducers/FooReducer'

const store = configureStore({
  reducer:  {
    foo: fooReducer,
  }
});

function MyApp({ Component, pageProps }: AppProps) {
  return (
  <Provider store={store}>
    <Component {...pageProps} />
  </Provider>
  )
}

export default MyApp

If you are familiar with NextJs, you will know that _app.tsx is the root component of the application. Any page loaded from /pages is rendered inside <Component {...pageProps} /> which means that all routes will always be within the <Provider/> component, allowing access to the global state(s) defined by store on any page.

4. Reading the global state

Go ahead and copy the following code inside your pages/index.tsx

// index.tsx
import type { NextPage } from 'next'
import { useSelector } from 'react-redux';

const Home: NextPage = () => {
  const foo = useSelector(state => state.foo.value);

  return (
    <main>
      <p>{foo.name}</p>
      <p>{foo.age}</p>
    </main>
  )
}

export default Home

When accessing the dev environment, we are now greeted with the text 'Nigel' and '63'. This is the initial state of the object we defined in FooReducer.tsx!

The useSelector() function this global state from the store we set up in our _app.tsx.

5. Writing to the global state

Edit your index.tsx and add the following code.

// index.tsx
//...
import { useDispatch } from 'react-redux';
import { agePlusOne } from '../src/reducers/FooReducer';

const Home: NextPage = () => {
  //...
  const dispatch = useDispatch();

  return (
    <main>
      {foo.name}
      <button onClick={() => {
        dispatch(agePlusOne(null));
      }}>Plus One</button>
      <br />
      {foo.age}
    </main>
  )
}

useDispatch() is the function which is used to execute the functions outlined in our FooReducer.tsx. Here, we have imported the agePlusOne function which adds 1 to the current age value. When we click the button, the age will increase by 1. The function takes no arguments.

If we want to do the same with the changeAll function, we must import it like we did with the agePlusOne function and call it with an argument of the new state that we want e.g. dispatch(changeAll({name: 'Bob', age: 44})).

Wrapping up

If you want to add more reducers, all you need to do is simply create additional components, for example BarReducer.tsx and then include it in the store constant defined in _app.tsx as below.

const store = configureStore({
  reducer:  {
    foo: fooReducer,
    bar: barReducer
  }
})

After doing this, you can then reference bar as you did with foo and have multiple global states!