Search code examples
reduxreact-reduxredux-toolkit

Dispatching an action or a reducer?


I will just note that I understand the standard data flow of Redux, which you have an action creator which notify the type to the reducer and the reducer will do the magic of updating the state.

yet in React-Redux (redux-toolkit) when I dispatch something it will be a reducer directly, thus conceptually the action creator is missing when I think about it and I'm struggling to understand how the 'reducer' determine the type of action.

in the example below I'm dispatching setRegistrationData which is a reducer not an action creator.

Some code examples to see if my code is incorrect rather then my understanding:

Dispatching the reducer:

dispatch(setRegistrationData())

Store:

const store : Store = configureStore({
  reducer: {
      modalWindow: modalWindowReducer,
      yPosition : windowYPositionSlice,
      loginData: loginDataSlice,
      formValidation : loginFormValidationSlice,
      registrationData : registrationDataSlice,
  }
})


// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>;
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch;
export default store;

A random Slice

// interface RegistrationData
interface RegistrationData{
    userName : string;
    email : string;
    password : string;
    confirmPassword : string;
}

// Need to change it to an object
const initialState : RegistrationData = {
    userName : '',
    email : '',
    password : '',
    confirmPassword : ''
};

export const registrationData = createSlice({
    name: 'registrationData',
    initialState,
    reducers: {
            setRegistrationData(state){
                Object.assign(state, { userName : 'DData', email : '[email protected]', password : 'dddd', confirmPassword : 'ddd'});
            }
    }
})


export const { setRegistrationData } = registrationData.actions;
export default registrationData.reducer;

Solution

  • Using RTK APIs you abstract some of the old Redux flow (e.g. Action Creators) but what happens under the hood is exactly the same old things.

    I think the source of your confusion comes specifically from createSlice method which is one of the most useful API methods RTK exposes:

    const registrationData = createSlice({
        name: 'registrationData',
        initialState,
        reducers: {
                setRegistrationData(state){
                    Object.assign(state, { userName : 'DData', email : '[email protected]', password : 'dddd', confirmPassword : 'ddd'});
                }
        }
    })
    

    What happens here is that you are writing inside the reducers object, a series of micro reducers ( officially named case reducers, since they are like the single case block inside a switch statement of a standard reducer ). The name they are given, will be assigned internally to automatically generated action creators. So in the end basically the result of createSlice is an object that contains a reducer and an actions properties and in fact that's what you export from the slice file through destructuring:

    export const { setRegistrationData } = registrationData.actions;
    

    The actions object contain the action creators that take the names of the case reducers written inside the reducers object, so when you dispatch setRegistrationData:

    dispatch(setRegistrationData(data))
    

    That (data) will automatically be sent to the setRegistrationData case reducer since the action.type is built-in in the action and data will become the payload of your action. In fact if you log the action inside the reducer:

    const registrationData = createSlice({
        name: 'registrationData',
        initialState,
        reducers: {
                setRegistrationData(state, action){
                    console.log(action) // {type: "registrationData/setRegistrationData", payload : { userName: ..., email: ..., etc... } }
                    Object.assign(state, { userName : 'DData', email : '[email protected]', password : 'dddd', confirmPassword : 'ddd'});
                }
        }
    })
    

    You see that the action has an automatically generated type with the shape of nameOfReducer/nameOfActionCreator.

    So as you can see the createSlice method just optimized the code for you and lets you write a lot less code, but under the hood it's the same if you would have written it by hand after all:

    const myReducer = (state, action) => {
    switch (action.type) {
    case "registrationData/setRegistrationData": return {...state, action.payload} // no IMMER
    case ... // some other types
      }
    }
    

    And then build the action creator yourself:

    const setRegistrationData = (data) => ({ type: "registrationData/setRegistrationData" , payload: data})
    

    And then dispatch it the same way:

    dispatch(setRegistrationData(data))
    

    That's more code, and you can't use IMMER which is built-in in createSlice method.

    So in the end it's just a matter of writing less code, more concise, easier to read and to maintain, but it doesn't change in any way how Redux works and its flow.