Note. Super simple Redux setup for React, Next.js and React Native apps, using Redux Toolkit (and optionally Redux Persist)

React Native Next.js Redux Setup Tutorial

I used React for years before embracing Redux, never quite getting the pattern and seeing it as a bit of an overly complicated dark art. Ironically I was making things much harder for myself by avoiding it.

When I finally sat down and figured it out (thanks in huge part to Redux Toolkit) I couldn’t believe what I had been missing out on. As a result my React apps are now so much more robust, easy to reason about, and scalable.

I personally learn best from reading example code, as opposed to digging through technical documentation. I’ve found a lot of tutorials on Redux setup to be a bit lacking - either configuring things in a more complex way than I require, or glossing over exactly what’s going on so I never truly understand what bits I need - so I thought I’d have a go at my own. This is pretty much a bare minimum as far as Redux goes, but I have rarely found much need for anything else.

What are Redux, Redux Toolkit and Redux Persist?

I won’t go too in-depth here, as if you’ve landed on this post you likely know the basics and just need some boilerplate to get it set up in your app, but the tl;dr is - Redux is a library (and general development pattern) which allows you to store and manage state globally, instead of on a component by component level.

This extracts a lot of data-related logic from your component files, allowing them to focus solely on rendering the UI and, critically, allows you to easily read from and write to any part of your state, from any of your components, without having to worry about passing loads of data and methods up and down through props.

Redux Toolkit is the real magic for me though - where Redux alone can be a little finicky to set up, Redux Toolkit (an additional library) massively simplifies the process and, to be honest, is pretty much the only reason I was ever able to figure out and embrace Redux full stop.

Finally, Redux Persist is an optional further library which allows you to easily “persist” (i.e. save) your data more permanently (across page refreshes, app closes and sessions). You won’t need this for every project, but when you do need to store data it’s great. The library helps you both to save your data in the first place, as well as “hydrate” your app (i.e. reload the data into state when a user returns).

These three libraries all work together seamlessly in both React and React Native, and while the depth of functionality they provide together may sound intimidating or complex, bootstrapping an app with them is actually surprisingly straightforward. So let’s get into it!

Our demo project

For the purposes of this demonstration I’ll use a barebones Next.js app as an example. But I’ve also used this exact pattern in plain React and React Native projects (with or without Expo) - it’s exactly the same regardless except for a couple of small details, which I'll mention as we go.

Step 1: Install dependencies

First things first, let’s get the Redux and Redux Toolkit libraries we need installed.

yarn add react-redux @reduxjs/toolkit

(Feel free to use npm or expo instead, it doesn't matter)

Step 2: Create your first “slice” of state

Welcome to Redux! The most common pattern I’ve come across for organising state is to store different chunks of state in separate “slices”. This is technically optional - you could store all of your state in one slice if you like - but separating things out doesn’t add much complexity, and with anything other than the smallest of apps you’ll benefit from splitting up your logic a bit.

So what does a slice look like? Well let’s imagine a note-taking app as an example - you may have 3 slices of state:

  • notesSlice - To store all of the user’s notes, folders and related data such as the note they’re currently editing, as well as reducers (functions) to add, update and delete notes from state.

  • progressSlice - To store the current state of the app. i.e. What view is the user currently looking at, is the settings modal open or closed?

  • settingsSlice - To store any user-defined settings which you want to provide to your users.

Within a slice you’ll define two main things - your initial state, and your “reducers”. A reducer is just a fancy word for a function which modifies your state. Redux Toolkit comes bundled with a package called Immer which makes modifying state extremely easy, as you’ll see in the example below.

So let’s create a new directory in our project root called “slices” and in there create a new file called progressSlice.js with some initial state and a couple of simple reducers.

slices/progressSlice.js

import { createSlice } from "@reduxjs/toolkit"

const progressSlice = createSlice({
  name: "progress",
  initialState: {
    settingsModalOpen: false,
    favouriteNotes: [],
  },
  reducers: {
    addFavourite: (state, action) => {
      state.favouriteNotes.push(action.payload)
    },
    toggleSettingsModal: (state) => {
      state.settingsModalOpen = !state.settingsModalOpen
    }
  },
})

export const {
  addFavourite,
  toggleSettingsModal
} = progressSlice.actions

export default progressSlice

Pretty simple, right? We give our slice a name, we define the different properties which we want to store for our app (a boolean, settingsModalOpen and an array, favouriteNotes), and we then define a couple of functions which we can call from anywhere in our app to update this state, addFavourite and toggleSettingsModal.

The reducers I’ve included here are very minimal - but you can carry out as much logic as you need in here, transforming data before storing it, firing off analytics events, making a POST request to a backend server to store data more permanently - whatever!

Each reducer receives two parameters - state and action. The state parameter is an object containing all of this slice’s current state. Through the magic of the Immer package which I mentioned before this can be modified directly to update your state, no need to worry about mutability.

The second parameter action contains any additional data which you need to send to state from your component within its payload property. As you’ll see later, when we call our addFavourite reducer from a component, we’ll want to be able to pass it the ID of the note we want to add to our favourites array. On the other hand I haven’t included the action parameter in my toggleSettingsModal reducer as it doesn’t require any data from the component, it simply switches a boolean value back and forth.

Finally we need to export our reducers, so that we can import these in any component which needs to use them.

In any new project I’m setting up, I will pretty much copy/paste the above file into each slice file and go from there, gradually adding whatever initial state and reducers make sense for that project.

Step 3: Configure your store

Next, we need to do a bit of boilerplating. This file is a bit more inscrutable than the last one - but this step is pretty much a set and forget situation. Once this file is doing what it needs to, you’ll rarely need to touch it.

So let’s create a new index.js file within our slices directory. This will take all of our slice files (in our example we only have one though), combine them into one global state object and export our final store, which is what our app will hook into in step 4.

slices/index.js

import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'
import { combineReducers } from 'redux'

import progressSlice from './progressSlice'

const rootReducer = combineReducers({
  progress: progressSlice.reducer,
  // any other reducers here
})

export const store = configureStore({
  reducer: rootReducer,
  middleware: getDefaultMiddleware({
    serializableCheck: {
      ignoredActions: [],
      ignoredPaths: [],
    },
  }),
})

Again, pretty simple. We import a few functions from Redux and Redux Toolkit, we use them to combine all of our slices into one rootReducer and we export it. combineReducers and configureStore are no doubt carrying out all kinds of magic for us, but we don’t really need to care.

As you’re building out your project and you start adding more and more slices, you’ll simply need to import them in here and add them to the object passed to combineReducers to make this new state available to your app.

Step 4: Provide your store to the rest of your app

Nearly done! At this point our state is configured, our store is configured - the only thing remaining is to actually make all of this juicy functionality available to our app.

Note that this is the first time in this tutorial that there will be any differentiation depending on what flavour of React you’re using (i.e. Next.js or Create React App or React Native etc) but all you’re really looking for is your top-level app component. For example, in Next.js this is pages/_app.js whereas in React Native this is App.js.

pages/_app.js (or your equivalent)

import { Provider } from 'react-redux'
import { store } from '../slices/index'

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

All we’re doing here is importing the store we created in Step 3, and passing it as prop to the <Provider> component which is given to us by Redux. The <Provider> component must wrap around all components which require access to our store.

Voila! Your entire app now has full access to all of your state slices and reducers.

Step 5: Start using state in your components

“But how do I actually read and modify my state?” I hear you whimper. Well that’s pretty easy too.

Let’s imagine we have a component within our app (this could be a small UI component, a Screen within React Native, a Page within Next.js - it doesn’t matter) which wants to read and modify the two state properties we defined in Step 2 of the tutorial.

components/MyComponent.js

import { useSelector, useDispatch } from "react-redux"
import { addFavourite, toggleSettingsModal } from "../slices/progressSlice"

export default function MyComponent() {
  const dispatch = useDispatch()
  const favouriteNotes = useSelector((state) => state.progress.favouriteNotes)
  const settingsModalOpen = useSelector(
    (state) => state.progress.settingsModalOpen
  )

  return (
    <>
      <section>
        <ul>
          {favouriteNotes.map((note) => {
            <li key={note}>{note}</li>
          })}
        </ul>
        <button onClick={() => dispatch(addFavourite(50))}>
          Add Favourite
        </button>
      </section>

      <section>
        <button onClick={() => dispatch(toggleSettingsModal())}>
          Toggle Settings Modal
        </button>
      </section>

      {settingsModalOpen && (
        <div>
          <h2>Settings</h2>
          <button onClick={() => dispatch(toggleSettingsModal())}>Close</button>
        </div>
      )}
    </>
  )
}

Here we meet the two key hooks which will let us work with state

  • useSelector lets us grab any value from state

  • useDispatch lets us call any of our reducer functions, which we defined in Step 2

If you’ve made it this far, I think the rest of the above file should be pretty self explanatory. We’re grabbing some state from our progressSlice and we’re conditionally rendering parts of our UI depending on their values. We’re then modifying some of these values in state using the reducers we created earlier (i.e. adding new favourites and toggling the modal) which, in classic React fashion, will also immediately be reflected in the UI.

Step 6: Save data locally using Redux Persist (Optional)

The first 5 steps in this guide should get you pretty far in your Redux journey, and will probably be adequate on their own for a lot of projects. However in certain circumstances you’ll want to make sure your user’s data is stored across multiple sessions, or if they refresh the page, which is where Redux Persist comes in. I use this exact pattern in my Website Launch Checklist project. I’m also using this in a React Native app which I’m currently developing to save all of the user’s data, progress and settings to their device.

Note that this guide purely covers local storage - so if the user clears their device (or uninstalls the app in the case of React Native) the data will be lost. Persisting data to a database is beyond the scope of this tutorial as there are a number of approaches, and will typically require a user account system in order to match up the active user to their records within your database.

Firstly, let’s add the Redux Persist dependency to our project.

yarn add redux-persist

(Feel free to use npm or expo instead, it doesn't matter)

This package handles all of the heavy lifting for us, so to get things persisting it’s really just a case of adding some extra bootstrapping to our root reducer and our top level component (see Steps 3 and 4 for our pre-persist starting point).

slices/index.js

import { configureStore } from "@reduxjs/toolkit"
import { combineReducers } from "redux"
import {
  persistStore,
  persistReducer,
  FLUSH,
  REHYDRATE,
  PAUSE,
  PERSIST,
  PURGE,
  REGISTER,
} from "redux-persist"
import storage from "redux-persist/lib/storage"
import progressSlice from "./progressSlice"

const persistConfig = {
  key: "root",
  version: 1,
  storage,
}

const rootReducer = combineReducers({
  progress: progressSlice.reducer,
})

const persistedReducer = persistReducer(persistConfig, rootReducer)

export const store = configureStore({
  reducer: persistedReducer,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: {
        ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
      },
    }),
})

export let persistor = persistStore(store)

pages/_app.js (or your equivalent)

import { Provider } from "react-redux"
import { store, persistor } from "../slices/index"
import { PersistGate } from "redux-persist/integration/react"

export default function MyApp({ Component, pageProps }) {
  return (
    <Provider store={store}>
      <PersistGate loading={null} persistor={persistor}>
        <Component {...pageProps} />
      </PersistGate>
    </Provider>
  )
}

As you can see we’re importing some additional functions from Redux Persist which slots nicely into our existing root reducer file, and then we’re layering another component <PersistGate> within our existing <Provider> component in _app.js. This component will delay the rendering of your app until any previous state has been hydrated, preventing any errors or a messy flash of unpopulated data.

Troubleshooting: Redux Persist not working with React Native?

Note that the default storage functionality provided in Redux Persist doesn’t work in all React Native environments. This is easily fixed though by swapping it with an alternative package AsyncStorage.

To do this, first install the package:

yarn add @react-native-async-storage/async-storage

(Feel free to use npm or expo instead, it doesn't matter)

Then in your root reducer file, import AsyncStorage instead of Redux Persist’s storage and include this in your persistConfig object. I’ve included the full updated file below for reference:

slices/index.js

import { configureStore } from "@reduxjs/toolkit"
import { combineReducers } from "redux"
import {
  persistStore,
  persistReducer,
  FLUSH,
  REHYDRATE,
  PAUSE,
  PERSIST,
  PURGE,
  REGISTER,
} from "redux-persist"
import AsyncStorage from '@react-native-async-storage/async-storage'
import progressSlice from "./progressSlice"

const persistConfig = {
  key: "root",
  version: 1,
  storage: AsyncStorage,
}

const rootReducer = combineReducers({
  progress: progressSlice.reducer,
})

const persistedReducer = persistReducer(persistConfig, rootReducer)

export const store = configureStore({
  reducer: persistedReducer,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: {
        ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
      },
    }),
})

export let persistor = persistStore(store)

You should now have fully functioning, persisted state management in your React project!

I hope this was useful. Please feel free to (civilly) email or tweet me with any feedback, comments or requests. My twitter handle is just below, and my email is in the footer.

Note
4th February 2022

Select side projects