MVC 和 MVVM 都是组织和管理应用程序代码结构的模式,具体实现和使用上有一些不同。MVC 更关注控制器和模型的交互,MVVM 更关注视图模型和试图的交互。
状态管理一般处于 MVC 中的模型层,MVVM 中的视图模型层。
状态提升是指将组件之间 共享的状态 移动到它们的 最近 共同祖先组件中进行管理的过程。通过将状态提升到更高层级的组件中,可以使得组件之间共享状态,实现状态的一致性和数据的传递。
使用场景:当多个子组件需要 访问/更新 相同的状态时,可以将这个 状态/状态的更新函数 提升到它们的最近共同祖先组件中进行管理,然后通过 props/回调函数 将 状态/更新函数 传递给子组件。
import React, {useState} from 'react';
const ParentComponent: React.FC = () => {
const [hiddenChildren, setHiddenChildren] = useState(false);
return (
<div>
<button onClick={() => setHiddenChildren((prev) => !prev)}>
{hiddenChildren ? '显示' : '隐藏'}
</button>
<Child1 hidden={hiddenChildren} />
<Child2 hidden={hiddenChildren} />
</div>
);
};
const Child1: React.FC<{hidden: boolean}> = ({hidden}) => {
return <div hidden={hidden}>Child1</div>;
};
const Child2: React.FC<{hidden: boolean}> = ({hidden}) => {
return <div hidden={hidden}>Child2</div>;
};
通常我们把包含不受控制状态的组件称为 “非受控组件”。即该组件状态由自己控制,其父组件无法控制状态。
相反,当组件中的重要信息是由 props 而不是其自身状态驱动时,就可以认为该组件是“受控组件”。这就允许父组件完全指定其行为。
非受控组件通常很简单,因为它们不需要太多配置。但是当你想把它们组合在一起使用时,就不那么灵活了。受控组件具有最大的灵活性,但它们需要父组件使用 props 对其进行配置。
在实践中,“受控”和“非受控”并不是严格的技术术语——通常每个组件都同时拥有内部状态和 props。然而,这对于组件该如何设计和提供什么样功能的讨论是有帮助的。
随着组件的不断迭代,组件中的状态会越来越复杂。为了降低状态的复杂程度,可以将逻辑移到组件外的 reducer 函数中。
useState 迁移到 useReducer:
function todosReducer(todos, action){
switch(action.type){
case 'add': {
return [...todos, {id: action.id, text: action.text}];
}
case 'update': {
return todos.map(todo => {
if (todo.id === action.id) {
return {id: action.id, text: action.text};
} else {
return todo;
}
});
}
case 'delete': {
return todos.filter(todo => todo.id !== action.id);
}
default: {
throw new Error(`Unhandled action type: ${action.type}`);
}
}
}
const Component: React.FC = () => {
// - const [todos, setTodos] = useState([]);
const [todos, dispatch] = useReducer(todosReducer, []);
return (
<div>
<button
onClick={() => dispatch({type: 'add', id: uuid(), text: 'todo1'})}
>
添加
</button>
{todos.map((todo) => (
<div key={todo.id}>
{todo.text}
<button onClick={() => dispatch({type: 'delete', id: todo.id})}>
删除
</button>
<button
onClick={() => {
dispatch({type: 'update', id: todo.id, text: 'newText'});
}}
>
修改
</button>
</div>
))}
</div>
);
};
使用 useReducer 的一些注意事项:
使用 Immer 代替 Reducer 和 State
对于修改对象和数组来说,可以使用 Immer 来简化 reducer。Immer 为你提供了一种特殊的 draft 对象,你可以通过它安全的修改 state。在底层,Immer 会基于当前 state 创建一个副本。这就是为什么通过 useImmerReducer 来管理 reducers 时,可以修改第一个参数,且不需要返回一个新的 state 的原因。
import React from "react";
import { useImmerReducer } from "use-immer";
function todosReducer(draft, action) {
switch (action.type) {
case 'add': {
return void draft.push({id: action.id, text: action.text});
}
case 'update': {
const index = draft.findIndex((todo) => todo.id === action.id);
return void draft.splice(index, 1, {id: action.id, text: action.text});
}
case 'delete': {
const index = draft.findIndex((todo) => todo.id === action.id);
return void draft.splice(index, 1);
}
default: {
throw new Error(`Unhandled action type: ${action.type}`);
}
}
}
...
import React from "react";
import { useImmer } from "use-immer";
function App() {
const [todos, updateTodos] = useImmer();
function addTodo(todo) {
updatePerson((draft) => {
draft.push(todo);
});
}
function deleteTodo(index) {
updatePerson((draft) => {
draft.splice(index, 1);
});
}
return <div>...</div>;
}
当父组件需要传递给子组件时,如果子组件层级过深,会导致需要通过许多中间组件传递 props,这不利于代码的可读性和维护。Context 允许父组件向无论多深的任何组件传递状态,而不需要 props 传递,子组件可以直接从 Context 中获取。
使用 Context:
import { createContext } from "react";
export const LevelContext = createContext(0);
import { useContext } from "react";
import { LevelContext } from "./LevelContext.js";
export default function Heading({ children }) {
const level = useContext(LevelContext);
// ...
}
import { LevelContext } from "./LevelContext.js";
export default function Section({ level, children }) {
return <LevelContext.Provider value={level}>{children}</LevelContext.Provider>;
}
完整示例可以查看 React Context
Flux 架构和函数式编程原理,状态可预测,可跟踪,允许在组件外使用(不依赖上下文)。 使用方式:创建并维护一个 store ,状态通知视图,action 触发 reducer 更新 store,视图通过订阅 store 获取状态。
简单示例:(摘自 Redux 官方文档,完整代码可以在 Redux 官方示例 中查看)
// ! 整个应用应保证只有一个 Store
import {configureStore, ThunkAction, Action} from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action<string>
>;
import {createAsyncThunk, createSlice} from '@reduxjs/toolkit';
import {RootState} from './store';
export interface CounterState {
value: number;
status: 'idle' | 'loading' | 'failed';
}
const initialState: CounterState = {
value: 0,
status: 'idle',
};
// redux 本身没有规定异步操作的处理方式,一般使用中间件来支持异步。
export const incrementAsync = createAsyncThunk(
'counter/fetchCount',
async (amount: number) => {
const response = await fetchCount(amount);
return response.data;
},
);
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
},
extraReducers: (builder) => {
builder
.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';
});
},
});
export const {increment, decrement} = counterSlice.actions;
export const selectCount = (state: RootState) => state.counter.value;
export default counterSlice.reducer;
function fetchCount(amount = 1) {
return new Promise<{data: number}>((resolve) =>
setTimeout(() => resolve({data: amount}), 500),
);
}
import {useDispatch, useSelector} from 'react-redux';
import {AppDispatch, RootState} from '@/src/store/store';
import {decrement, increment, selectCount} from '@/src/store/counterSlice';
export default function App() {
const useAppSelector = useSelector.withTypes<RootState>();
const useAppDispatch = useDispatch.withTypes<AppDispatch>();
const dispatch = useAppDispatch(); // action
const count = useAppSelector(selectCount); // 监听数据
return (
<div>
<button
className='button'
aria-label='Decrement value'
onClick={() => dispatch(decrement())}
>
-
</button>
<span className='text-sm font-semibold'>{count}</span>
<button
className='button'
aria-label='Increment value'
onClick={() => dispatch(increment())}
>
+
</button>
</div>
);
}
小型全局状态管理库,Context 和订阅机制的结合。useState+useContext 的替代品,在组件颗粒度较细的情况下很有优势。
import { atom, useAtom } from "jotai";
const countAtom = atom(0);
const [count, setCount] = useAtom(countAtom);
import { atom } from "jotai";
const countAtom = atom(0);
const readOnlyAtom = atom((get) => get(countAtom) * 2);
const writeOnlyAtom = atom(null, (get, set, update) => {
set(countAtom, get(countAtom) - update.count);
});
简单示例:
import { atom, useAtom } from "jotai";
const countAtom = atom(0);
export default function App() {
const [count, setCount] = useAtom(countAtom);
return (
<div>
<button className="button" aria-label="Decrement value" onClick={() => setCount(count - 1)}>
-
</button>
<span className="text-sm font-semibold">{count}</span>
<button className="button" aria-label="Increment value" onClick={() => setCount(count + 1)}>
+
</button>
</div>
);
}
状态管理不是必须的,不应该滥用状态管理。
更多关于 React 状态管理可以查阅 here