RTK provide these most commonly used APIs:
configureStore
createSlice
createAsyncThunk
: abstracts the standard "dispatch actions before/after an
async request" patterncreateEntityAdapter
: prebuilt reducers and selectors for CRUD operations on
normalized statecreateSelector
: a re-export of the standard Reselect API for memoized
selectorscreateListenerMiddleware
: a side effects middleware for running logic in
response to dispatched actionsnpm create vite@latest
npm install @reduxjs/toolkit react-redux
npx msw init public/ --save
More on: https://mswjs.io/
// app/store.ts
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
// Create a Redux store
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// main.tsx
import { store } from './app/store';
import { Provider } from 'react-redux';
root.render(
<Provider store={store}>
<App />
</Provider>
);
Note that Redux requires us not to mutate state, but RTK's createSlice
and
createReducer
APIs use Immer
internally to allow us to "mutate" state(it
creates copies under the hood).
// features/counter/counterSlice.ts
import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
export interface CounterState {
value: number;
}
const initialState: CounterState = {
value: 0,
};
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
We can also export something else from slice files, such as selectors:
export const selectCount = (state: RootState) => state.counter.value;
In some cases, we can caste the initial state by using as
, see:
https://redux-toolkit.js.org/tutorials/typescript#define-slice-state-and-action-types
// features/counter/Counter.ts
import React from 'react';
import type { RootState } from '../../app/store';
import { useSelector, useDispatch } from 'react-redux';
import { decrement, increment } from './counterSlice';
export function Counter() {
const count = useSelector((state: RootState) => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<div>
<button
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
Increment
</button>
<span>{count}</span>
<button
aria-label="Decrement value"
onClick={() => dispatch(decrement())}
>
Decrement
</button>
</div>
</div>
);
}
So we do not have to import the RootState
and AppDispatch
types into each
component.
// app/hooks.ts
import { useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
export const useAppSelector = useSelector.withTypes<RootState>();
A collection of Redux reducer logic and actions for a single feature of an app, typically defined together in a single file. The name comes from splitting up the root Redux state object into multiple "slices" of state.
createSlice
automatically generates action creators:
console.log(counterSlice.actions.increment());
// {type: "counter/increment"}
createSlice
also generates the slice reducer function:
const newState = counterSlice.reducer(
{ value: 10 },
counterSlice.actions.increment()
);
console.log(newState);
// {value: 11}
As createSlice uses a library called Immer
inside, we can "mutate" state
inside the reducer
.
useSelector
hook.action
has been dispatched and the Redux store has been
updated,useSelector
will re-run the selector function, it forces the
component to re-render if we return a new reference value.// Selector functions allows us to select a value from the Redux root state.
// Selectors can also be defined inline in the `useSelector` call
// in a component, or inside the `createSlice.selectors` field.
export const selectCount = (state: RootState) => state.counter.value;
export const selectStatus = (state: RootState) => state.counter.status;
A thunk is a Redux function that can contain asynchronous logic. We can use them the same way we use a typical Redux action creator:
await store.dispatch(incrementAsync(6));
dispatch
and getState
as argumentsredux-thunk
middleware by defaultredux-thunk
middleware (a type of plugin for
Redux) be added to the Redux store when it's created. Redux Toolkit's
configureStore
already sets it up automatically.Thunks are written using two functions:
// the outside "thunk creator" function
const fetchUserById = (userId: string): AppThunk => {
// the inside "thunk function"
return async (dispatch, getState) => {
try {
dispatch(userPending());
// make an async call in the thunk
const user = await userAPI.fetchById(userId);
// dispatch an action when we get the response back
dispatch(userLoaded(user));
} catch (err) {
// If something went wrong, handle it here
}
};
};
Redux Toolkit provides a createAsyncThunk
API to implement the creation and
dispatching of actions describing an async request
When we dispatch this thunk, it will dispatch a pending
action before making
the request, and either a fulfilled
or rejected
action after the async logic
is done.
export const incrementAsync = createAsyncThunk(
'counter/fetchCount',
async (amount: number) => {
const response = await fetchCount(amount);
return response.data;
}
);
Arguments Explanation
We can also define Thunks Inside of createSlice: https://redux.js.org/tutorials/essentials/part-5-async-logic#optional-defining-thunks-inside-of-createslice
We handle the thunk created by createAsyncThunk
in
createSlice.extraReducers
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
// omit reducers
},
// The `extraReducers` field lets the slice handle actions defined elsewhere,
// including actions generated by createAsyncThunk or in other slices.
extraReducers: (builder) => {
builder
// Handle the action types defined by the `incrementAsync` thunk defined below.
// This lets the slice reducer update the state with request status and results.
.addCase(incrementAsync.pending, (state) => {
state.status = 'loading';
})
.addCase(incrementAsync.fulfilled, (state, action) => {
state.status = 'idle';
state.value += action.payload;
})
.addCase(incrementAsync.rejected, (state) => {
state.status = 'failed';
});
},
});
Use extraReducers
to handle actions that were defined outside of the slice.
which means:
For example:
{
users: {
ids: ["user1", "user2", "user3"],
entities: {
"user1": {id: "user1", firstName, lastName},
"user2": {id: "user2", firstName, lastName},
"user3": {id: "user3", firstName, lastName},
}
}
}
The returned object contains:
.setAll
In reducers(We known it contains Immer), you can either "mutate" an existing state object, or return a new state value yourself, but not both at the same time.
A function that generates memoized selectors that will only recalculate results when the inputs change
export const selectPostsByUser = createSelector(
// Pass in one or more "input selectors"
[
// we can pass in an existing selector function that
// reads something from the root `state` and returns it
selectAllPosts, // or (state: RootState) => selectAllPosts(state),
// and another function that extracts one of the arguments
// and passes that onward
(_state: RootState, userId: string) => userId,
],
// the output function gets those values as its arguments,
// and will run when either input value changes
(posts, userId) => posts.filter((post) => post.user === userId)
);
or like this:
export const selectPostsByUser = createSelector(
selectAllPosts,
(_state: RootState, userId: string) => userId,
(posts, userId) => posts.filter((post) => post.user === userId)
);
React's default behavior is that when a parent component renders, React will recursively render all child components inside of it!
Because of the immutable update, for the reference type state, each update will create a new reference, which will cause a re-render.
https://redux.js.org/tutorials/essentials/part-6-performance-normalization#investigating-the-posts-list
Solutions:
// This could cause circular dependency when listenerMiddleware depend on
// something from current file
import { type AppStartListening } from '@/app/listenerMiddleware';
// use this instead:
import type { AppStartListening } from '@/app/listenerMiddleware';
Manually dispatching an RTKQ request thunk will create a subscription entry, we might need to unsbscribe from that data later, otherwise the data stays in the cache permanently.
RTK Query uses a "document cache" approach, not a "normalized cache". We can use
createEntityAdapter
to normalize the response data, but this is not the same
thing as a "normalized cache" - we only transformed how this one response is
stored rather than deduplicating results across endpoints or requests.
Object.entries
, Object.values
satisfies
// Workaround: cast state instead of declaring variable type
const initialState = {
value: 0,
} satisfies CounterState as CounterState;