[React 进阶系列] React Context 案例学习:使用 TS 及 HOC 封装 Context
具体 context 的实现在这里:[React 进阶系列] React Context 案例学习:子组件内更新父组件的状态。
根据项目经验是这样的,自从换了 TS 之后,就再也没有二次封装过了使用 TS 真的可以有效解决 typo 和 intellisense 的问题
这里依旧使用一个简单的 todo 案例去完成
使用 TypeScript
结构方面采用下面的结构:
❯ tree src
src
├── App.css
├── App.test.tsx
├── App.tsx
├── context
│ └── todoContext.tsx
├── hoc
├── index.css
├── index.tsx
├── logo.svg
├── models
│ └── todo.type.ts
├── react-app-env.d.ts
├── reportWebVitals.ts
└── setupTests.ts
4 directories, 11 files
创建 type
这里的 type 指的是 Todo 的类型,以及 context 类型,这是一个简单案例,结构就不会特别的复杂:
-
todo.type.ts
export type ITodo = { id: number; title: string; description: string; completed: boolean; };
-
todoContext.tsx
import { ITodo } from '../models/todo.type'; export type TodoContextType = { todos: ITodo[]; addTodo: (todo: ITodo) => void; removeTodo: (id: number) => void; toggleTodo: (id: number) => void; };
这种 type 的定义,我基本上说 component 在哪里就会定义在哪里,而不会单独创建一个文件在
model
下面去实现,当然,这种其实也挺看个人偏好的……
创建 context
这里主要就是提供一个 context,以供在其他地方调用 useContext
而使用:
export const TodoContext = createContext<TodoContextType | null>(null);
这里 <>
是接受 context 的类型,我个人偏向会使用一个具体的 type 以及 null
。其原因是 JS/TS 默认没有初始化和没有实现的变量都是 undefined
,也因此使用 undefined
的指向性不是很明确。
而使用 null
代表这个变量存在,因此更具有指向性
虽然在 JS 实现中一般我都偷懒没设置默认值……
没有报错真的会忘……超小声 bb
创建 Provider
Provider 的实现如下:
const TodoProvider: FC<{ children: ReactNode }> = ({ children }) => {
const [todos, settodos] = useState<ITodo[]>([
{
id: 1,
title: 'Todo 1',
completed: false,
description: 'Todo 1',
},
{
id: 2,
title: 'Todo 2',
completed: false,
description: 'Todo 1',
},
]);
const addTodo = (todo: ITodo) => {
const newTodo: ITodo = {
id: todos.length + 1,
title: todo.title,
description: todo.description,
completed: false,
};
settodos([...todos, newTodo]);
};
const removeTodo = (id: number) => {
const newTodos = todos.filter((todo) => todo.id !== id);
settodos(newTodos);
};
const toggleTodo = (id: number) => {
const newTodos = todos.map((todo) => {
if (todo.id === id) {
return { ...todo, completed: !todo.completed };
}
return todo;
});
settodos(newTodos);
};
return (
<TodoContext.Provider
value={{
todos,
addTodo,
removeTodo,
toggleTodo,
}}
>
{children}
</TodoContext.Provider>
);
};
export default TodoProvider;
其实也没什么复杂的,主要就是一个 FC<ChildPops>
这个用法,这代表当前的函数是一个 Functional Component,它只会接受一个参数,并且它的参数会是一个 ReactNode
添加 helper func
如果想要直接使用 const {} = useContext(TodoContest)
的话,TS 会报错——默认值是 null
。所以这个时候可以创建一个 helper func,保证返回的 context 一定有值:
export const useTodoContext = () => {
const context = useContext(TodoContext);
if (!context) {
throw new Error('useTodoContext must be used within a TodoProvider');
}
return context;
};
这样可以保证调用 useTodoContext
一定能够获取值。两个错误对比如下:
不过这个时候页面渲染还是有一点问题,因为没有提供对应的 provider:
使用 HOC
一般的解决方法有两种:
-
直接在
Main
上嵌套一个 Provider这个的问题就在于,
Main
本身就需要调用 context 中的值,如果在这里嵌套的话就会导致Main
组件中无法使用 context 中的值 -
在上层组件中添加对应的 provider
这样得到 App 层去修改,可以,但是有的情况并不是一个非常的适用,尤其是多个 Provider 嵌套,而其中又有数据依赖的情况下,将 Provider 一层一层往上推意味着创建多个 component 去实现
使用 HOC 的方法是兼具 1 和 2 的解决方案,具体实现如下:
import { ComponentType } from 'react';
import TodoProvider, { TodoContextType } from '../context/todoContext';
const withTodoContext = (WrappedComponent: ComponentType<any>) => () =>
(
<TodoProvider>
<WrappedComponent />
</TodoProvider>
);
export default withTodoContext;
这样 Main 层面的调用如下:
import React from 'react';
import { Button } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import { useTodoContext } from '../context/todoContext';
import withTodoContext from '../hoc/withTodoContext';
const Main = () => {
const { todos } = useTodoContext();
return (
<div className="todo-main">
<input type="text" />
<Button className="add-btn">
<AddIcon />
</Button>
<br />
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
</div>
);
};
export default withTodoContext(Main);
补充一下,如果 HOC 需要接受参数的话,实现是这样的:
const withExampleContext =
(WrappedComponent: ComponentType<any>) => (moreProps: ExampleProps) =>
(
<ExampleProvider>
<WrappedComponent {...moreProps} />
</ExampleProvider>
);
export default withExampleContext;
这样导出的方式还是使用 withExampleContext(Component)
,不过上层可以用 <Component prop1={} prop2={} />
的方式向 Component
中传值
调用
完整实现如下:
const Main = () => {
const [newTodo, setNewTodo] = useState('');
const { todos, addTodo, toggleTodo } = useTodoContext();
const onChangeInput = (e: React.ChangeEvent<HTMLInputElement>) => {
setNewTodo(e.target.value);
};
const onAddTodo = () => {
addTodo({
title: newTodo,
description: '',
completed: false,
});
setNewTodo('');
};
const onCompleteTodo = (id: number) => {
toggleTodo(id);
};
return (
<div className="todo-main">
<input type="text" value={newTodo} onChange={onChangeInput} />
<Button className="add-btn" onClick={onAddTodo}>
<AddIcon />
</Button>
<br />
<ul>
{todos.map((todo) => (
<li
key={todo.id}
style={{
textDecoration: todo.completed ? 'line-through' : 'none',
cursor: 'pointer',
}}
onClick={() => onCompleteTodo(todo.id)}
>
{todo.title}
</li>
))}
</ul>
</div>
);
};
export default withTodoContext(Main);
效果如下:
TS 提供的自动提示如下: