Today I learned how to use @reduxjs/toolkit, as well as Redux-related performance optimizations.
The following is the best pratices of Redux Reducer immutability.
default case of a reducer returns the original stateThe default case of a reducer returns the original state, which can avoid other components re-rendering
jsconst PersonReducer = (state = { age: 12, name: "zs" }, action) => {
switch (action.type) {
case "P_ADD":
// ✅ When the `state` actually changes, a new object must be created (immutability principle).
return { ...state, age: state.age + 1 };
default:
// ✅ When the `state` has not changed, returning a reference to the original state is the correct approach
// This is the best practice recommended by Redux, for the following reasons:
// 1. Redux uses shallow equality(浅比较) to determine if the state has changed.
// 2. If the references are the same, it means there's no change, and unnecessary re-rendering won't be triggered.
// 3. Creating a new object every time would cause all connected components to re-render (performance issue).
// 4. The core of immutability is "not modifying the original object," not "always creating a new object."
// 不可变性的核心是"不修改原对象",而不是"总是创建新对象"
return state;
// ❌ other components will re-render
// such CounterComp, which is introduced CounterReducer
// return {...state}
}
};
Why do this?
Object.is()) to determine if the state has changed.📚 Core Principles
| - | - |
|---|---|
| The essence of immutability: | It avoids modifying the original object, instead of always creating a new one. |
| When to create a new object: | Only when the state truly changes. |
| When to return the original reference: | Returns the original reference when the state has not changed. |
| Redux's comparison mechanism: | Uses shallow comparison (reference comparison), not deep comparison. |
Issue: If mapStateToProps returns the entire state, any change to the state will trigger a component re-render.
Optimization: Only select the fields the component actually needs.
js// ❌ Error: Returning the entire state
const mapStateToProps = (state) => state;
// ✅ Correct: Select only the fields you need.
const mapStateToProps = (state) => ({
count: state.counter, // only select `counter`
});
shallowEqual for shallow comparisonIssue: By default, connect uses strict equality (===) comparisons, while shallow comparisons are required for objects.
Optimization: Use shallowEqual for shallow comparison.
typescriptimport { shallowEqual } from "react-redux";
const PersonComp = connect(mapStateToProps, mapDispatchToProps, null, {
areStatesEqual: shallowEqual,
areOwnPropsEqual: shallowEqual,
})(Component);
Effect: The object is only re-rendered when its properties actually change.
TODO check it
React.memo prevents unnecessary re-renderingIssue: When the parent component re-renders, the child component also re-renders (even if the props haven't changed).
Optimization: Use React.memo to wrap the component.
jsconst ExpensiveComponent = React.memo(function ExpensiveComponent({ data }) {
return <div>{data.value}</div>;
});
// Or define a custom comparison function.
const ExpensiveComponent = React.memo(
function ExpensiveComponent({ data }) {
return <div>{data.value}</div>;
},
(prevProps, nextProps) => {
// Custom comparison logic
return prevProps.data.value === nextProps.data.value;
}
);
Effect: Only re-render when props actually change.
Issue: All the code is packaged together, resulting in a slow initial load.
Optimization: Using React.lazy and Suspense to split code。
typescriptimport React, { Suspense, lazy } from "react";
// Dynamically imported components
const LazyComponent = lazy(() => import("./LazyComponent"));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}
Effect: Load on demand to reduce the initial package size.
jsconst UseStateUsage = lazy(() => import("./hook/UseStateUsage"));
// 加载状态组件
const Loading = () => (
<div style={{ padding: "50px", textAlign: "center" }}>
<div>🔄 加载组件中...</div>
</div>
);
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<BrowserRouter>
{/* 关键:必须用 Suspense 包裹懒加载组件 */}
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/UseStateUsage" element={<UseStateUsage />}></Route>
{/* 这里也可以放置非懒加载的路由 */}
</Routes>
</Suspense>
</BrowserRouter>)
Issue: Rendering long list items can cause performance issues.
Optimization: Use the virtualization library to render only the visible items.
jsimport { FixedSizeList } from "react-window";
function VirtualizedList({ items }) {
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{({ index, style }) => <div style={style}>
{items[index].name}
</div>}
</FixedSizeList>
);
}
Effect: Rendering only the visible area and nearby elements significantly improves performance.
References
Getting Started with Domo
tsimport { configureStore } from "@reduxjs/toolkit";
import { CounterReducer } from "./counterSlice";
// this is a wrapper around the basic Redux Create Store Function
// It autumatically sets up a store with right defaults.
// For example, it automatically turns on the Redux Dev Tools Extensions.
// it automatically adds the Thunk middleware
// and automatically turns on a couple of development checks that catch common mistakes,
// like accidental mutations.
export const store = configureStore({
// @reduxjs/toolkit has also do combineReducers automatically
reducer: {
counter: CounterReducer,
},
});
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
tsimport { createSlice, type PayloadAction } from "@reduxjs/toolkit";
interface ICounterState {
value: number;
}
const initialState: ICounterState = { value: 0 };
const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
incremented(state) {
// it's okay to do this, beacuse immer.js makes it immutable
state.value += 1;
},
amountAdded(state, actions: PayloadAction<number>) {
state.value += actions.payload;
},
},
});
export const { incremented, amountAdded } = counterSlice.actions;
export const CounterReducer = counterSlice.reducer;
import { type Dispatch } from "redux";
export const incrementAsync = (amount: number) => (dispatch: Dispatch) => {
setTimeout(() => {
dispatch(amountAdded(amount));
}, 1000);
};
tsimport {
type TypedUseSelectorHook,
useSelector,
useDispatch,
} from "react-redux";
import type { AppDispatch, RootState } from "./store";
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
tsximport { Provider } from "react-redux";
import { store } from "./store";
import { incremented, amountAdded, incrementAsync } from "./counterSlice";
import { useAppDispatch, useAppSelector } from "./hooks";
export default function App() {
return (
<Provider store={store}>
<Counter />
</Provider>
);
}
function Counter() {
const counter = useAppSelector((state) => state.counter);
const dispatch = useAppDispatch();
function handleClick() {
dispatch(incremented());
}
return (
<div>
<p>{counter.value}</p>
<button type="button" onClick={handleClick}>
ADD-
</button>
<button type="button" onClick={() => dispatch(amountAdded(5))}>
ADD by 5
</button>
<button type="button" onClick={() => dispatch(incrementAsync(2))}>
Async ADD 2
</button>
</div>
);
}
Q: Why we should use @reduxjs/toolkit?
The Explanation from redux's maintainer
- connect is really really hard to use with Typescript
and "useSelector" "useDispatch" are trivial- In older versions of Redux, we couldn't modify existing state because the UI wouldn't render. So in toolkit we use immer.js, it actually one kind of replays them and turns it into a safe and correct, immutable update, as if we'd done all of that copying and spreading and mapping and everything ourselves(但是使用了 Redux Toolkit, 它使用了 immer.js,它包裹了 state 更新和追踪所有 mutations, 实际上,它以一种特殊的方式重放这些画面,并将其转化为安全且正确的画面。)
| 对比维度 | 传统 Redux | Redux Toolkit(RTK) |
|---|---|---|
| TS 支持 | 需手动写大量类型定义,繁琐 | 原生深度适配,自动推导类型,零冗余类型代码 |
| 核心依赖 | 需手动安装 thunk、immer 等 | 内置 thunk、immer、reselect,零配置开箱即用 |
| 模板代码量 | 多(3 + 文件 / 状态模块) | 极少(1 文件 / 状态模块,createSlice 整合所有) |
| 不可变更新 | 手动解构 / Object.assign,易出错 | 直接修改 draft,immer 自动转不可变,无学习成本 |
| 异步逻辑处理 | 手动定义异步 action,复杂 | createAsyncThunk+extraReducers,优雅处理异步状态 |
| 调试体验 | 需手动配置 devtools | 内置适配,直接接入,支持异步状态回溯 |
| 团队协作成本 | 高(规范难统一) | 低(强制最佳实践,代码风格统一) |
supplement: redux chrome devtools
tsximport React from "react";
import { useSelector, shallowEqual } from "react-redux";
import { type RootState } from "./store";
const UserCounterDisplay: React.FC = () => {
const selectorResult = useSelector(
(state: RootState) => ({
username: state.user.username,
isLoggedIn: state.user.isLoggedIn,
// count: state.counter.value,
}),
// if this has no shallowEqual, when the state.counter.value changes, this component will be rerender
// if this has shallowEqual, it will not rerender when state.counter.value is changed
shallowEqual // ← key to performance optimization
);
console.log("UserCounterDisplay 渲染了!", selectorResult);
return (
<div
style={{
padding: "20px",
border: "2px solid #4caf50",
margin: "10px 0",
borderRadius: "8px",
}}
>
<h3>用户信息与计数器(使用了 shallowEqual)</h3>
<p>用户名:{selectorResult.username}</p>
<p>登录状态:{selectorResult.isLoggedIn ? "已登录" : "未登录"}</p>
{/* <p>计数:{selectorResult.count}</p> */}
</div>
);
};
export default UserCounterDisplay;
Continue to supplement the cases based on the previous examples.
tsxconst UserCounterDisplay: React.FC = () => {
const doubleCount = useSelector((state: RootState) => {
// if counter hasn't change ,but user has change,
// this selector will also exec
console.log("doubleCount exec");
return state.counter.value * 2;
});
return <div>
<p>double count:{doubleCount}</p>
</div>
}
after optimization
tsximport { doubleCountSelector } from "./counterSlice";
const UserCounterDisplay: React.FC = () => {
const doubleCount = useSelector(doubleCountSelector);
return <div>
<p>double count:{doubleCount}</p>
</div>
}
tsconst count = (state: RootState) => state.counter.value;
export const doubleCountSelector = createSelector([count], (count) => {
// if counter hasn't change ,but user has change,
// this selector will not exec
console.log("doubleCountSelector exec", count);
return count * 2;
});
Advanced usage


supplement: in old version of redux , we should install reselect(npm install reselect)
jsimport { createSelector } from "reselect";
const selectCounter = (state) => state.counter;
// 记忆化选择器:只有当 counter 改变时才重新计算
const selectCounterDouble = createSelector(
[selectCounter],
(counter) => counter * 2
);
const mapStateToProps = (state) => ({
doubleCount: selectCounterDouble(state),
});
The complete syntax of createStore
jsimport { createStore } from 'redux';
const store = createStore(reducer, preloadedState, enhancer);
usage
js// reducer.js
const initialState = { count: 0, name: '默认名称' }; // reducer 默认初始状态
function rootReducer(state = initialState, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
default:
return state;
}
}
// store.js
// 预加载状态(结构需与 reducer 状态一致)
const preloadedState = { count: 10, name: '预加载名称' };
const store = createStore(rootReducer, preloadedState);
console.log(store.getState()); // 输出:{ count: 10, name: '预加载名称' }(覆盖了 reducer 默认初始状态)
Redux state本文作者:郭郭同学
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!