Highlights:
- Solve the complexities of write Redux (actions, reducers, selector...middleware...)
- Solve the immutable pattern with nested spread operator by using Immer
- Wrap Redux-thunk for async action
- Using Entity pattern
createSlice
createSlice
returns a "slice" object that contains the generated reducer function as a field namedreducer
, and the generated action creators inside an object calledactions
.
import { createSlice } from '@reduxjs/toolkit'; export const counterSlice = createSlice({ name: 'counter', // also prefix for actions initialState: { value: 0, }, reducers: { increment: state => { // Redux Toolkit allows us to write "mutating" logic in reducers. It // doesn't actually mutate the state because it uses the Immer library, // which detects changes to a "draft state" and produces a brand new // immutable state based off those changes state.value += 1; }, decrement: state => { state.value -= 1; }, incrementByAmount: (state, action) => { state.value += action.payload; }, }, }); export const { increment, decrement, incrementByAmount } = counterSlice.actions; // The function below is called a thunk and allows us to perform async logic. It // can be dispatched like a regular action: `dispatch(incrementAsync(10))`. This // will call the thunk with the `dispatch` function as the first argument. Async // code can then be executed and other actions can be dispatched export const incrementAsync = amount => dispatch => { setTimeout(() => { dispatch(incrementByAmount(amount)); }, 1000); }; // The function below is called a selector and allows us to select a value from // the state. Selectors can also be defined inline where they're used instead of // in the slice file. For example: `useSelector((state) => state.counter.value)` export const selectCount = state => state.counter.value; export default counterSlice.reducer;
createAsyncThunk
Mainly, you don't want to write "GET_BOOKS", "GET_BOOKS_FULFILLED", "GET_BOOKS_ERROR" actions.
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit' import { userAPI } from './userAPI' // First, create the thunk const fetchUserById = createAsyncThunk( 'users/fetchByIdStatus', async (userId, thunkAPI) => { const response = await userAPI.fetchById(userId) return response.data } ) // Then, handle actions in your reducers: const usersSlice = createSlice({ name: 'users', initialState: { entities: [], loading: 'idle' }, reducers: { // standard reducer logic, with auto-generated action types per reducer }, extraReducers: { // Add reducers for additional action types here, and handle loading state as needed [fetchUserById.fulfilled]: (state, action) => { // Add user to the state array state.entities.push(action.payload) } } }) // Later, dispatch the thunk as needed in the app dispatch(fetchUserById(123))
createEntityAdapter
Ideas / api come from @ngrx/entity. Export you APIs to deal with data CRUD. Just toolkit using Immer as internal API.
import { createEntityAdapter, createSlice, configureStore, } from '@reduxjs/toolkit' type Book = { bookId: string; title: string } const booksAdapter = createEntityAdapter<Book>({ // Assume IDs are stored in a field other than `book.id` selectId: (book) => book.bookId, // Keep the "all IDs" array sorted based on book titles sortComparer: (a, b) => a.title.localeCompare(b.title), }) const booksSlice = createSlice({ name: 'books', initialState: booksAdapter.getInitialState(), reducers: { // Can pass adapter functions directly as case reducers. Because we're passing this // as a value, `createSlice` will auto-generate the `bookAdded` action type / creator bookAdded: booksAdapter.addOne, booksReceived(state, action) { // Or, call them as "mutating" helpers in a case reducer booksAdapter.setAll(state, action.payload.books) }, }, }) const store = configureStore({ reducer: { books: booksSlice.reducer, }, }) type RootState = ReturnType<typeof store.getState> console.log(store.getState().books) // { ids: [], entities: {} } // Can create a set of memoized selectors based on the location of this entity state const booksSelectors = booksAdapter.getSelectors<RootState>( (state) => state.books ) // And then use the selectors to retrieve values const allBooks = booksSelectors.selectAll(store.getState())