Redux Toolkit Basics. Learn Modern Redux Fast

Redux & Redux Toolkit

Redux-toolkit is the modern way to write redux. If you haven't used Redux Toolkit before, I suggest you go read the Getting Started Guide, and at minimum you should understand createSlice, createAction, createAsyncThunk from the API.

Mandatory documentation:

In order for you to fully grasp the power of Redux Toolkit, go checkout these docs:

1. Redux toolkit - Usage with Typescript. Read this twice

Usage With TypeScript | Redux Toolkit

2.createAsyncThunk:

createAsyncThunk | Redux Toolkit

3.createSlice:

createSlice | Redux Toolkit

Folder structure

We define an entity as a slice of state. For example, a user entity, a post entity, a comment entity, etc. Each entity will have its own slice of state, and its own reducer, actions, and selectors.

**Note:**The directory entity does not exist in the actual project, it is a generalisation in order to showcase the structure of files.

store / //directory where all state management logic is stored
  modules / //directory where all entities are stored
  entity /
  slice.ts.actions.ts // File where the slice and reducer are defined // File where all the actions are defined
selectors.ts // File where all the selectors are defined
index.ts // File for exports management

To avoid circular dependencies inside slices DON'T declare actions inside the slice file as unknown behaviour might occur. Even though this is not specified in the docs, once your project grows, you will encounter this problem if you declare everything in the same file.

Usage

Let's say that in the future, a new type of entity will be used: Monument and users can access, search and save monuments in their profile. Let's first define the type of a monument

enum ApiStatus {
  Idle = 'idle',
  Loading = 'loading',
  Success = 'success',
  Error = 'error',
}

interface Monument {
  id: number // primary ID property
  name: string

  apiStatus: ApiStatus // status of fetching entity from the server
}

1. Create the entity directory

First off we create a new directory named monuments in the app/store/modules directory.

2. Create a new slice for the entity

Than we create a slice.ts :

import {createSlice} from '@reduxjs/toolkit';
import {isErrorPayload} from '@utils/typeGuards';
import {ApiStatus} from 'types';

/**
* Type for dictionary where we keep fetched monuments
* for fast lookup by Id.
* In this dictionary the key is the id and the value is
* the monument object with that id. Example: Get monument
* with id 5 => localDictionary[5] (the monument object or undefined)
*/
type LocalDictionary = Record<number, Monument | undefined>

interface MonumentState {
 byId: number[] // an array to store monuments by Id
 local: LocalDictionary
 apiStatus: ApiStatus // status for fetching many monuments at once
 error?: string //optional error to display in case of fetching errors
}

const initialState: MonumentState = {
	byId: []
  local: {}
  apiStatus: ApiStatus.Idle,
};

const monumentSlice = createSlice({
  name: 'monument',
  initialState, // defined on top
  reducers: {},
  extraReducers: (builder) => {
    // This is where we will define most reducers for extra type safety
  },
});

export default monumentSlice.reducer;

3. Define an action

Now that we have the slice. Let's define anaction that fetches a monument from the server and stores it in the slice. We will usecreateAsyncThunk to do that. We now create an actions.ts file inside the monuments directory. If the code below seems confusing please read the link from above on createAsyncThunk:

import * as monumentAPI from '@api/monument' // functions for fetching monument data
import { createAsyncThunk } from '@reduxjs/toolkit'
import { ThunkApi } from 'types'

/* Async action to fetch a monument from the server */
export const fetchMonument = createAsyncThunk<Monument, number, ThunkApi>(
  'monuments/fetchOne',
  async (monumentId, thunkAPI) => {
    try {
      // fetch from the server
      const monument = await monumentAPI.getById(monumentId)
      // return with status success
      return {
        ...monument,
        apiStatus: ApiStatus.Success,
      }
    } catch (error) {
      return thunkAPI.rejectWithValue(error)
    }
  }
)

4. Handle action inside slice

We defined the action but we haven't specified how should redux handle this action. If you stop here and call dispatch(fetchMonument(14)) the theoretical Api Call will launch but the returned data would not be stored in the global state. Let's define that behaviour back in slice.ts :

import * as actions from './actions'
import {addNewValuesToLocalState} from '@utils/redux';

...
const monumentSlice = createSlice({
  name: 'monument',
  initialState, // defined on top
  reducers: {},

  // Use extraReducers, not reducers. Why?
  // With this style of adding reducers the (state, action) pair
  // is already typed with the definitions from the action file.
  // This is faster AND safer than declaring how to handle the action in the
  // reducers prop as their you need to define the TS types yourself.
  extraReducers: (builder) => {

    // handle what the state should be when the monument starts fetching
		builder.addCase(actions.fetchMonument.pending, (state, action) => {
      //mark the individual monument as pending

      // get the id that was passed as parameter to action
      const {arg: monumentId} = state.meta

      // retrieve existent monument object or create new one
      const monument: Partial<Monument> = {
        ...(state.local[monummentId] ?? {},
        apiStatus: ApiStatus.Pending
      }

      //save value with pending to state
      addNewValuesToLocalState(state.local, [monument as Monument])
    }
  },
});

...

So now when we dispatch fetchMonument , our local reducer will mark that particular monument as being in a loading state. Next let's define what happens when the monument has been fetched:

...
 extraReducers: (builder) => {

    // handle what the state should be when the monument starts fetching
		builder.addCase(actions.fetchMonument.pending, (state, action) => {
    ...
    }
    // handle what the state should be when the monument has been fetched
    builder.addCase(actions.fetchMonument.success, (state, action) => {
       const fetchedMonument = action.payload; // already typed because of builder function

       // we already set apiStatus to ApiStatus.Success when we
       // returned from the action
       addNewValuesToLocalState(state.local, [fetchedMonument])
    }

}

Great! Now we have a functional action that fetches a resource and stores it in the global state to be used by components.

But what happens if an error occurs?!

Let's define that case too:

import {isPayloadError} from '@utils/typeGuards';
...
 extraReducers: (builder) => {

    // handle what the state should be when the monument starts fetching
		builder.addCase(actions.fetchMonument.pending, (state, action) => {...}

    // handle what the state should be when the monument has been fetched
    builder.addCase(actions.fetchMonument.success, (state, action) => {...}

    // handle what the state should be when the api call failed
    builder.addCasse(actions.fetchMonument.rejected, (state, action) => {
      const {payload} = action;
      // type guard to determine if there is actually an error message
      if (isPayloadError(payload) {
	      state.error = payload.message
      }
       // get the id that was passed as parameter to action
      const {arg: monumentId} = state.meta

      // retrieve existent monument object or create new one
      const failedMonuent: Partial<Monument> = {
        ...(state.local[monummentId] ?? {}),
        apiStatus: ApiStatus.Error // mark fetching failed
      }
      //save value with pending to state
      addNewValuesToLocalState(state.local, [failedMonuent as Monument])
    })
}

Awesome! We have a fully usable redux action to fetch individual monuments.

5. Getting data from redux

Now that we have our action let's use it in a MonumentScreen . What we want to do:

  1. Get the monument from the redux store
  2. If the monument is not fetched, fetch it
  3. Display monument information

Let's start:

import * as React from 'react'
import {useSelector, useDispatch} from 'react-redux'
import * as monumentActions from '@redux/modules/monuments/actions'

interface ScreenProps {
  monumentId: number
}
function MonumentScreen(props: ScreenProps) {
    const {monumentId} = props;

    // Get the current value from the store
    const monument = useSelector<StoreState>(state =>
          state.monuments.local[monumentId] ?? {apiStatus: ApiStatus.Idle})

    // Helpful status indicators
    const isIdle = monument.apiStatus === ApiStatus.Idle;
    const isLoading = monument.apiStatus === ApiStatus.Loading;
    const isError = monument.apiStatus === ApiStatus.Error;
    const isSuccess = monument.apiStatus === ApiStatus.Success;

    // redux dispatch prop
    const dispatch = useDispatch();

    // If the monument is not fetched dispatch the action to fetch it
    React.useEffect(() => {
      if (isIdle) {
          dispatch(monumentActions.fetchMonument(monumentId))
        }
    }, [isIdle, dispatch]

    // Placeholder for loading monument
    if (isLoading) {
       return <LoadingMonument />
    }

    // Feedback in case of error
    if (isError) {
       return <Text>Something went wrong...</Text>
    }

    // fetch was succesfull!
    return <View>
             <Text>{monument.name}</Text>
           </View>

}

Let's evaluate what happens in the code above.

  1. In the first moment we try to extract from redux a monument that doesn't exist so we get back {apiStatus: ApiStatus.Idle}
  2. In the useEffect hook we check if the status is idle we dispatch the fetch action
  3. The pending part of action triggers so now our data extracted from redux is {apiStatus: ApiStatus.Loading}
  4. The api finishes loading from the server and the success part of fetchMonument is triggered in redux. Therefore apiStatus is equal to ApiStatus.Success making the isSuccess value be true and display the final data to the screen. Yaaay!

Great! Everything works ok. But we have a problem with code reusability and the fact that we can just extract all that logic into a useMonument hook. Let's do that:

export function useMonument(monumentId: number) {
    // Get the current value from the store
    const monument = useSelector<StoreState>(state =>
          state.monuments.local[monumentId] ?? {apiStatus: ApiStatus.Idle})
    // redux dispatch prop
    const dispatch = useDispatch();

    // Helpful status indicators
    const isIdle = monument.apiStatus === ApiStatus.Idle;
    const isLoading = monument.apiStatus === ApiStatus.Loading;
    const isError = monument.apiStatus === ApiStatus.Error;
    const isSuccess = monument.apiStatus === ApiStatus.Success;

    // If the monument is not fetched dispatch the action to fetch it
    React.useEffect(() => {
      if (isIdle) {
        dispatch(monumentActions.fetchMonument(monumentId))
      }
    }, [isIdle, dispatch]

    return {
      monument,
      isIdle,
      isError,
      isSuccess
    }
}

As you can see, no code has been rewritten, just copy and paste into a hook value.

And now our MonumentScreen becomes:

import {useMonument} from '@hooks/monument'
...
function MonumentScreen(props: ScreenProps) {
  const {monumentId} = props;

  const {monument, isLoading, isError, isIdle, isSuccess} = useMonument(
    monumentId,
  );

  // Placeholder for loading monument
  if (isLoading || isIdle) {
    return <LoadingMonument />;
  }

  // Feedback in case of error
  if (isError) {
    return <Text>Something went wrong...</Text>;
  }

  // fetch finished successfully!
  if (isSuccess) {
    return (
      <View>
        <Text>Hello from {monument.name}</Text>
      </View>
    );
  }

  // (Optional) Safety throw if none of the above cases match
  throw new Error('This part of the function should not be reachable');
}

Looks way better now. And now we have a reusable hook to use throughout the application and by other developers to further speed up development. Great!

Keep this structure in mind for other async redux actions you might be building as it is easier to read and highly reusable.