How do I define an action without having to implement a reducer for it?

Viewed 879

I'm converting some existing redux code to the toolkit way. We have a lot of actions that trigger thunks (to load data from backend) but dont have a reducer. Our pattern being the load/success/fail triple. Basically only the success and fails need a reducer statement. How do I do this with the toolkit? Do I have to put in a reducer that just returns the unchanged state for the load actions?

2 Answers

With redux-toolkit you have a few options here...

1. Existing thunks + RTK actions

If you only need to update one slice of your store with the loaded data, you can create “success” and “fail” actions in the reducers property on that slice. Then, change your thunk to dispatch those instead of the old success/fail actions.

const slice = createSlice({
  name: 'data',
  initialState: {},
  reducers: {
    fetchDataSuccess(state, action) {
      // Do something with the response
    },
    fetchDataError(state, action) {
      // Do something with the error
    }
  }
}

const { fetchDataSuccess, fetchDataError } = slice.actions

export function fetchData() {
  return dispatch => api.getData()
    .then(response => dispatch(fetchDataSuccess(response.data)))
    .catch(error => dispatch(fetchDataError(error))
}

export default slice.reducer

2. Existing thunks + extraReducers

If you don't want to refactor the existing thunk, or if the actions will be used across multiple slices, you can use the extraReducers property.

// These can also be defined in a separate file and imported
const FETCH_SUCCESS = 'data/FETCH_SUCCESS'
const FETCH_FAIL = 'data/FETCH_FAIL'

export function fetchData() {
  return dispatch => api.getData()
    .then(response => dispatch({ type: FETCH_SUCCESS, payload: response.data }))
    .catch(error => dispatch({ type: FETCH_FAIL, payload: error }))
}

const slice = createSlice({
  // ... the usual properties
  extraReducers: {
    [FETCH_SUCCESS](state, action) {
      // Do something with the response
    },
    [FETCH_FAIL](state, action) {
      // Do something with the error
    }
  }
}

3. createAsyncThunk

This approach is similar to the above, but the createAsyncThunk utility handles a lot of it for you, like catching errors, dispatching the actions at the right time, etc.

const fetchData = createAsyncThunk(
  'data/fetchData',
  () => api.getData().then(response => response.data)
)

const slice = createSlice({
  // ... the usual properties
  extraReducers: {
    [fetchData.fulfilled](state, action) {
      // Do something with the response
    },
    [fetchData.rejected](state, action) {
      // Do something with action.error
    }
  }
}

// Components still call this like a normal function: fetchData()
export { fetchData }

export default slice.reducer

Whichever way you end up going, if you're not using the "load" action (or .pending from createAsyncThunk), you don't need to add it to either reducers or extraReducers.

I think you can simply create thunk-actions.ts (or eg. saga-actions.ts) file to keep actions that trigger data loading.

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

export const fetchUserComments = createAction<{ id: string }>(
    'fetchUserComments',
);

all actions that have reducer's logic will be generated by slice

Related