62

I have learned about redux toolkit for 2 months, and in createSlice have reducers and extrareducers, I know they use to change state from dispatch but I don't know the difference, Where should we use them?

Yilmaz
  • 35,338
  • 10
  • 157
  • 202
kalipts
  • 1,097
  • 1
  • 9
  • 21

6 Answers6

89

The reducers property both creates an action creator function and responds to that action in the slice reducer. The extraReducers allows you to respond to an action in your slice reducer but does not create an action creator function.

You will use reducers most of the time.

You would use extraReducers when you are dealing with an action that you have already defined somewhere else. The most common examples are responding to a createAsyncThunk action and responding to an action from another slice.

Linda Paiste
  • 38,446
  • 6
  • 64
  • 102
42

extrareducers is actually like reducers with enhancements, but it's been built to handle more options, esecially other actions (like the ones generated in other slices or actions made by createAction or createAsyncThunk). In one word

Everything you can dispatch in redux can be added to it.

The extrareducers property in createSlice can be used as a function or as an object.

The function form has an input argument named builder.

Example 1: The builder.addCase function adds an action from another slice (with Typescript type safety).

const {actions,reducer} = createSlice({
  name:'users',
  reducers:{
    ......
  },
  initialState:.... ,
  extraReducers:(builder) => {
    builder.addCase(authActions.logout, state => {
      state.isLoggedIn = false;
    });
  }
});

The authActions.logout here is another action from another slice.

If you create an action using createAsyncThunk function (which is imported from "@reduxjs/toolkit") You can handle loading, success & failure states.

Example 2: Handling loading state from an async Thunk:

const fetchUserById = createAsyncThunk(
  'users/fetchByIdStatus',
  async (userId, thunkAPI) => {
    const response = await userAPI.fetchById(userId)
    return response.data
  }
)

const {actions,reducer} = createSlice({
  name:'users',
  reducers:{
    ......
  },
  initialState:.... ,
  extraReducers: (builder) => {
    ......
    ......

    builder.addCase(fetchUserById.pending, (state, action) => {
      state.loading=true;
      state.whoseDataIsLoading = action.payload;
    })
  }
});
  1. fetchUserById.pending (handles loading state of the asyncThunk)
  2. fetchUserById.rejected (handles failed state)
  3. fetchUserById.fulfilled (handle success state)

The builder also accepts addDefaultCase and addMatcher in which the addDefaultCase acts as the default case in switch statement used by conventional reducers (reducers in the redux without toolkit) and,

addMatcher is used with a type predicate function a function used to infer that some input has some particular type (type of action in here) to ensure the action has a particular type, then the state changes.

Example 3: The extrareducers as an object:

const {actions,reducer} = createSlice({
  name:'users',
  reducers:{
    ......
  },
  initialState:.... ,
  extraReducers: {
    ['incrementBy']: (state, action) => {
      state.someValue += action.payload
    }
  }
  });

References:

  1. https://redux-toolkit.js.org/api/createSlice#extrareducers

  2. https://redux-toolkit.js.org/usage/usage-with-typescript#type-safety-with-extrareducers

  3. https://redux-toolkit.js.org/api/createAsyncThunk

  4. https://redux-toolkit.js.org/usage/usage-with-typescript#typing-builderaddmatcher

  5. https://redux-toolkit.js.org/api/createReducer#builderadddefaultcase

Amir Gorji
  • 2,809
  • 1
  • 21
  • 26
23

let's say you have those two reducers:

const booksSlice = createSlice({
    name: "books",
    initialState: [],
    reducers: {
      addBook(state, action) {
        // this state is not global state. it is only books slice
        state.push(action.payload);
      },
      resetBooks(state, action) {
        // immers assume whatever you return you want your state to be
        return [];
      },
    },});

 export const { addBook, resetBooks } = booksSlice.actions;


  const authorsSlice = createSlice({
    name: "authors",
    initialState: [],
    reducers: {
      addAuthor(state, action) {
        state.push(action.payload);
      },
      resetAuthors(state, action) {
        return [];
      },
    },});

    export const { addAuthor, resetAuthors } =authorsSlice.actions;

Both reducers have initial state is []. Let's say you display "books" and "authors" list in a page and you want to have a button to reset the both state.

<button onClick={() => handleResetLists()}>
    Clear the page
  </button>

inside handleResetLists you can dispatch both actions

const handleReset = () => {
    dispatch(resetBooks());
    dispatch(resetAuthors());
  };

But instead, I could tell authorsSlice to watch for the additional action types with extraReducers

const authorsSlice = createSlice({
  name: "authors",
  initialState: [],
  reducers: {
    addAuthor(state, action) {
      state.push(action.payload);
    },
    // builder tells this slice to watch for additional action types
    extraReducers(builder) {
      // second argument will update the state
      builder.addCase(booksSlice.actions.reset, () => {
        return [];
      });
    },
  },
});

Now inside handleReset function, I will dispatch an action to book reducer but author reducer will be watching for this action and it will also update

const handleReset = () => {
    dispatch(resetBooks());
    // I do not need to dispatch this
    // dispatch(resetAuthors()); 
  };

In the above implementation, authorsSlice is dependent on booksSlice because we use booksSlice.actions.reset inside extraReducer. Instead we create a separate action because what if we get rid of bookSlice in the future

import {createAction} from "@reduxjs/toolkit"
// we are gonna dispatch this
export const resetAction=createAction("lists/reset")

inside both slicer add this extraReducer:

 extraReducers(builder) {
      builder.addCase(resetAction, (state, action) => {
        state = [];
      });
    },

inside the page

 import {resetAction} from "./store/actions"

 const handleReset = () => {
    dispatch(reset());
    // I do not need to dispatch this
    // dispatch(resetAction()); 
  };
  • Note that extraReducer is not part of the actions that generated by the slice.actions
Yilmaz
  • 35,338
  • 10
  • 157
  • 202
12

If action is supposed to be handled by one reducer, use reducers.

If action is supposed to be handled by multiple reducers, use extraReducers.

Yuvraj Patil
  • 7,944
  • 5
  • 56
  • 56
0

Suppose, you have one TodoReducer.js file in that you have manageTodo reducer:

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

const initialState = {
  todo: [],
};

export const manageToDo = createSlice({
  name: 'Todo',
  initialState: initialState,

  reducers: {
    add: (state, action) => {
      console.log(action);
      state.todo = [...state.todo, action.payload];
    },
    del: (state, action) => {
      state.todo.splice(action.payload, 1);
    },
    edit: (state, action) => {
      state.todo.splice(action.payload.index, 1, action.payload.item);
    },
    clearTodo: state => {
      state.todo = [];
    },
  },
});

You want that, On add todo there is increment in counter & On delete todo there is decrement in counter so you can utilise the extraReducer Like This:

CounterReducer.js

import {createSlice} from '@reduxjs/toolkit';
import {manageToDo} from './TodoReducer';

const counterInitialState = {
  counter: 0,
};

const manageCounter = createSlice({
  name: 'Counter',
  initialState: counterInitialState,
  reducers: {
    increment: (state, action) => {
      console.log('testing increment', state.counter);
      state.counter++;
    },
    decrement: (state, action) => {
      state.counter--;
    },
  },
  extraReducers: builder => {
    builder.addCase(manageToDo.actions.clearTodo, () => {
      return 0;
    });
    builder.addCase(manageToDo.actions.add, state => {
      state.counter++;
    });
    builder.addCase(manageToDo.actions.del, state => {
      state.counter--;
    });
  },
});

export const manageCounterReducer = manageCounter.reducer;
export const {increment, decrement} = manageCounter.actions;
0

ExtraReducer is reducer and its role is to be reducer. I learned and used it with createAsyncThunk(). Role of (extra)reducer is to be pure function with two parameters actual state and action. Reducer is one slice of reducers, but extraReducer are three slice of reducer, pending, fullfiled and reject. Every slice of (extra)reducer is action creator.
Every action creator are being dispatch to store.
Thunk is function in middleware pattern. Thunk is action creator which create three more action creator: pending, fullfiled and reject. These three action creators are three extraReducers what immutably update state three time. More: https://www.youtube.com/watch?v=2M-HR2J88S4

b2ok
  • 544
  • 6
  • 13