使用 React Hooks 进行状态管理的最佳方法
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
React Hooks已经存在一段时间了,所以我借此机会探索了如何使用它们来处理状态管理。我的目标是了解在实际应用和大规模部署中,哪些方法有效,哪些无效。
我回顾了一些现有的选项和技术,并附上了一些解释和批评。你可以在这个GitHub仓库中找到其中一些示例。
基础示例
在开始之前,让我们先描述一下我们将在后续章节中使用的初始组件。
假设我们有一个待办事项应用程序。它的容器和组件结构简化如下:
首先是Header包含待办事项列表输入表单的部分。
type HeaderProps = {
addTodo?: (text: string) => void;
}
const Header = ({ addTodo }: HeaderProps ) => {
const onSave = (text: string) => {
if (text.length !== 0) {
addTodo && addTodo(text);
}
};
return (
<header className="header">
<h1>todos</h1>
<TodoTextInput
newTodo={true}
onSave={onSave}
placeholder="Tell me what you want to do!"
/>
</header>
)
}
在哪里:
type TodoTextInputProps = {
text?: string
editing?: boolean;
placeholder?: string;
onSave: (text: string) => void;
newTodo: boolean;
}
type TodoTextInputState = {
text: string;
}
export class TodoTextInput extends React.Component<TodoTextInputProps, TodoTextInputState> {
state = {
text: this.props.text || ''
};
handleSubmit = (e: React.KeyboardEvent<any>) => {
const text = e.currentTarget.value.trim();
if (e.which === 13) { // Enter Key
this.props.onSave(text);
if (this.props.newTodo) {
this.setState({ text: '' });
}
}
};
handleChange = (e: React.FormEvent<HTMLInputElement>) => {
this.setState({ text: e.currentTarget.value });
};
handleBlur = (e: React.FormEvent<HTMLInputElement>) => {
if (!this.props.newTodo) {
this.props.onSave(e.currentTarget.value);
}
};
render() {
return (
<input
className={classnames({
edit: this.props.editing,
"new-todo": this.props.newTodo
})}
type="text"
placeholder={this.props.placeholder}
autoFocus={true}
value={this.state.text}
onBlur={this.handleBlur}
onChange={this.handleChange}
onKeyDown={this.handleSubmit}
/>
);
}
}
然后是MainSection显示待办事项的地方:
type MainSectionProps = {
todos: Todo[];
deleteTodo: (id: number) => void;
editTodo: (id: number, text: string) => void;
toggleTodo: (id: number) => void;
}
const MainSection = ({
todos,
deleteTodo,
editTodo,
toggleTodo,
}: MainSectionProps) => {
return (
<section className="main">
<TodoList
todos={todos}
deleteTodo={deleteTodo}
editTodo={editTodo}
toggleTodo={toggleTodo}
/>
</section>
);
};
type TodoListProps = MainSectionProps
const TodoList = ({ todos, editTodo, deleteTodo, toggleTodo }: TodoListProps) => (
<ul className="todo-list">
{todos.map((todo: Todo) => (
<TodoItem
key={todo.id}
todo={todo}
editTodo={editTodo}
toggleTodo={toggleTodo}
deleteTodo={deleteTodo}
/>
))}
</ul>
);
在哪里:
type TodoItemProps = Pick<MainSectionProps, 'toggleTodo' | 'deleteTodo' | 'editTodo'> & {
todo: Todo;
}
type TodoItemPropsState = {
editing: boolean;
}
export class TodoItem extends React.Component<TodoItemProps, TodoItemPropsState> {
state = {
editing: false
};
handleDoubleClick = () => {
this.setState({ editing: true });
};
handleSave = (id: number, text: string) => {
if (text.length === 0) {
this.props.deleteTodo(id);
} else {
this.props.editTodo(id, text);
}
this.setState({ editing: false });
};
render() {
const { todo, toggleTodo, deleteTodo } = this.props;
let element;
if (this.state.editing) {
element = (
<TodoTextInput
text={todo.text}
editing={this.state.editing}
onSave={text => this.handleSave(todo.id, text)}
newTodo={false}
/>
);
} else {
element = (
<div className="view">
<input
className="toggle"
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<label onDoubleClick={this.handleDoubleClick}>{todo.text}</label>
<button className="destroy" onClick={() => deleteTodo(todo.id)} />
</div>
);
}
return (
<li
className={classnames({
completed: todo.completed,
editing: this.state.editing
})}
>
{element}
</li>
);
}
}
这段代码非常典型,在任何 TodoMVC 的在线示例中都能找到。请注意,我们使用回调函数将所有逻辑都移到了更高级别的组件中。
现在让我们来看看使用 React hooks 进行状态管理的几种最流行方法。
自定义钩子状态
这是最直接的方法。我们提供了一个自定义钩子,它将提供容器所需的所有必要业务逻辑,例如:
type Todo = {
id: number;
completed: boolean;
text: string;
}
const useTodos = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const addTodo = (text: string) => {
setTodos([
...todos,
{
id: todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
completed: false,
text
}
]);
};
const deleteTodo = (id: number) => {
setTodos(todos.filter(todo => todo.id !== id));
};
const editTodo = (id: number, text: string) => {
setTodos(todos.map(todo => (todo.id === id ? { ...todo, text } : todo)));
};
const toggleTodo = (id: number) => {
setTodos(
todos.map(
todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
return [
todos,
{
addTodo,
deleteTodo,
editTodo,
toggleTodo,
}
];
};
const App = () => {
const [
todos,
{ addTodo, deleteTodo, editTodo, toggleTodo }
]: any = useTodos();
return (
<div>
<Header addTodo={addTodo} />
<MainSection
todos={todos}
deleteTodo={deleteTodo}
editTodo={editTodo}
toggleTodo={toggleTodo}
/>
</div>
);
};
这里所有状态管理逻辑都封装在一个可重用的useTodos钩子函数中。我们返回待办事项列表以及与之对应的操作列表。当我们调用其中一个修改函数时,待办事项列表会更新,并且整个列表会重新渲染,如下例所示:
我喜欢它的一点是:简单易用。没有中央存储,也没有复杂的配置。我们直接就能用。
我不喜欢的地方:它的简洁性。在大型应用或处理复杂数据集时,它的扩展性可能不佳。但对于处理小范围领域的小型程序来说,它非常完美。
自定义 Hooks + React 上下文
本文基于之前文章中阐述的理念,并结合自定义钩子来管理状态:
import React from "react";
import { useState, useMemo, useContext } from "react";
import { Todo } from "../Example5";
const AppContext = React.createContext({});
/**
* Our custom React hook to manage state
*/
type AppState = {
todos: Todo[];
};
const useAppState = () => {
const initialState: AppState = { todos: [] };
// Manage the state using React.useState()
const [state, setState] = useState<AppState>(initialState);
// Build our actions. We'll use useMemo() as an optimization,
// so this will only ever be called once.
const actions = useMemo(() => getActions(setState), [setState]);
return { state, actions };
};
// Define your actions as functions that call setState().
// It's a bit like Redux's dispatch(), but as individual
// functions.
const getActions = (
setState: React.Dispatch<React.SetStateAction<AppState>>
) => ({
deleteTodo: (id: number) => {
setState((prevState: AppState) => ({
...prevState,
todos: prevState.todos.filter((todo: Todo) => todo.id !== id)
}));
},
editTodo: (id: number, text: string) => {
setState((prevState: AppState) => ({
...prevState,
todos: prevState.todos.map((todo: Todo) =>
todo.id === id ? { ...todo, text } : todo
)
}));
},
toggleTodo: (id: number) => {
setState((prevState: AppState) => ({
...prevState,
todos: prevState.todos.map((todo: Todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
}));
},
addTodo: (text: string) => {
setState((prevState: AppState) => ({
...prevState,
todos: [
...prevState.todos,
{
id:
prevState.todos.reduce(
(maxId, todo) => Math.max(todo.id, maxId),
-1
) + 1,
completed: false,
text
}
]
}));
}
});
// Sub-components can use this function. It will pick up the
// `state` and `actions` given by useAppState() higher in the
// component tree.
const useAppContext = (): any => {
return useContext(AppContext);
};
export { AppContext, useAppState, useAppContext };
然后我们可以这样使用它:
const TodoList: React.FC = () => {
const { state, actions } = useAppContext();
return (
<div>
<Header addTodo={actions.addTodo} />
<MainSection
todos={state.todos}
deleteTodo={actions.deleteTodo}
editTodo={actions.editTodo}
toggleTodo={actions.toggleTodo}
/>
</div>
);
};
const App: React.FC = () => {
const { state, actions } = useAppState();
return (
<AppContext.Provider value={{ state, actions }}>
<div>
<TodoList />
</div>
</AppContext.Provider>
);
};
export default App;
在上面的例子中,我们将动作与状态分离,并使用全局变量AppContext作为这些值的提供程序。这样,任何组件都可以调用useAppContext该全局变量来获取并使用该上下文。
我喜欢的地方:将操作与状态分离。使用React.ContextAPI 是对之前示例的改进。
我不喜欢的地方:我们可能需要进一步的定制。例如,我们需要对操作或状态进行逻辑命名空间划分。总的来说,这是一个不错的解决方案。
Redux + Hooks + 代理
最后一个例子是基于本文解释的理念
。这里我们保留了熟悉的 Redux store,其中包含所有的 reducer、初始状态等等:
import { createStore } from 'redux';
import { Todo } from './models';
export type AppState = {
todos: Todo[];
};
const reducer = (state = AppState, action: any) => {
switch (action.type) {
case 'ADD_TODO':
return { ...state, todos: [
...state.todos,
{
id: state.todos.reduce((maxId: number, todo: Todo) => Math.max(todo.id, maxId), -1) + 1,
completed: false,
text: action.text
}
] };
case 'DELETE_TODO':
return { ...state, todos: state.todos.filter((todo: Todo) => todo.id !== action.id) };
case 'TOGGLE_TODO':
return { ...state, todos: state.todos.map((todo: Todo) =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
) };
case 'EDIT_TODO':
return { ...state, todos: state.todos.map((todo: Todo) =>
todo.id === action.id ? { ...todo, text: action.text } : todo
) };
default:
return state;
}
};
const store = createStore(reducer);
export default store;
接下来,我们需要编写以下这段冗长的代码来执行所有逻辑:
import React, { useContext, useEffect, useReducer, useRef, useMemo } from 'react';
const ReduxStoreContext = React.createContext({});
export const ReduxProvider = ({ store, children }: any) => (
<ReduxStoreContext.Provider value={store}>
{children}
</ReduxStoreContext.Provider>
);
export const useReduxDispatch = () => {
const store: any = useContext(ReduxStoreContext);
return store.dispatch;
};
const forcedReducer = (state: any) => !state;
const useForceUpdate = () => useReducer(forcedReducer, false)[1];
export const useReduxState = () => {
const forceUpdate: any = useForceUpdate();
const store: any = useContext(ReduxStoreContext);
const state = useRef(store.getState());
const used: any = useRef({});
const handler = useMemo(() => ({
get: (target: any, name: any) => {
used.current[name] = true;
return target[name];
},
}), []);
useEffect(() => {
const callback = () => {
const nextState = store.getState();
const changed = Object.keys(used.current)
.find(key => state.current[key] !== nextState[key]);
if (changed) {
state.current = nextState;
forceUpdate();
}
};
const unsubscribe = store.subscribe(callback);
return unsubscribe;
}, []);
return new Proxy(state.current, handler);
};
教程文章中有详细的解释。一旦我们理解了这种逻辑,就可以像这样使用它:
const App: React.FC = () => (
<ReduxProvider store={store}>
<TodoList />
</ReduxProvider>
);
const TodoList: React.FC = () => {
const state = useReduxState();
const dispatch = useReduxDispatch();
const addTodo = useCallback((text: string) => dispatch({ type: 'ADD_TODO', text: text, }), []);
const deleteTodo = useCallback((id: number) => dispatch({ type: 'DELETE_TODO', id: id, }), []);
const editTodo = useCallback((id: number, text: string) =>
dispatch({ type: 'EDIT_TODO', id: id, text: text }), []);
const toggleTodo = useCallback((id: number) => dispatch({ type: 'TOGGLE_TODO', id: id }), []);
return (
<div>
<Header addTodo={addTodo} />
<MainSection
todos={state.todos}
deleteTodo={deleteTodo}
editTodo={editTodo}
toggleTodo={toggleTodo}
/>
</div>
);
};
当然,我们可以将所有调度操作提取到一个单独的地方,使用选择器等等,但大部分功能都是类似的。
我喜欢的地方:它能很好地与现有的 Redux store、actions 和 reducers 配合使用。
我不喜欢的地方:那边那些乱七八糟的东西看起来很奇怪。我们不确定这会对性能造成什么影响。IE11 不支持代理。
呼,终于写完了。希望这篇文章能帮助大家理解如何使用 React Hooks 来管理状态。总的来说,我认为 React Hooks 在状态管理方面与 Redux 非常契合,既满足特定用途,又方便易用。在三个例子中,我比较喜欢最后一个,因为它允许我保留 Redux store。
那么你呢?你能分享一些使用 React Hooks 进行可扩展状态管理的例子吗?或者你认为它们更好?
文章来源:https://dev.to/theodesp/best-ways-to-use-react-hooks-for-state-management-44h3
