• [React 进阶系列] React Context 案例学习:使用 TS 及 HOC 封装 Context


    [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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    创建 type

    这里的 type 指的是 Todo 的类型,以及 context 类型,这是一个简单案例,结构就不会特别的复杂:

    • todo.type.ts

      export type ITodo = {
        id: number;
        title: string;
        description: string;
        completed: boolean;
      };
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
    • 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;
      };
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8

      这种 type 的定义,我基本上说 component 在哪里就会定义在哪里,而不会单独创建一个文件在 model 下面去实现,当然,这种其实也挺看个人偏好的……

    创建 context

    这里主要就是提供一个 context,以供在其他地方调用 useContext 而使用:

    export const TodoContext = createContext<TodoContextType | null>(null);
    
    • 1

    这里 <> 是接受 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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    其实也没什么复杂的,主要就是一个 FC 这个用法,这代表当前的函数是一个 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;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这样可以保证调用 useTodoContext 一定能够获取值。两个错误对比如下:

    在这里插入图片描述

    在这里插入图片描述

    不过这个时候页面渲染还是有一点问题,因为没有提供对应的 provider:

    在这里插入图片描述

    使用 HOC

    一般的解决方法有两种:

    1. 直接在 Main 上嵌套一个 Provider

      这个的问题就在于,Main 本身就需要调用 context 中的值,如果在这里嵌套的话就会导致 Main 组件中无法使用 context 中的值

    2. 在上层组件中添加对应的 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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这样 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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    补充一下,如果 HOC 需要接受参数的话,实现是这样的:

    const withExampleContext =
      (WrappedComponent: ComponentType<any>) => (moreProps: ExampleProps) =>
        (
          <ExampleProvider>
            <WrappedComponent {...moreProps} />
          </ExampleProvider>
        );
    
    export default withExampleContext;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这样导出的方式还是使用 withExampleContext(Component),不过上层可以用 的方式向 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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    效果如下:

    在这里插入图片描述

    TS 提供的自动提示如下:

    在这里插入图片描述

  • 相关阅读:
    第二章--应用层
    解决 VSCode 配置远程连接,过程试图写入的管道不存在
    应急响应 >> 网络安全应急事件类型和处置办法
    MLC--机器学习编译的课程笔记
    Linux:centos9的本地yum仓库配置
    vue3.0 如何自定义指令
    动作捕捉技术识别细小错位 机械外骨骼适应性升级
    虚拟机与主机互传文件方法分享
    内存模型 C++
    使用maven模板快速生成项目
  • 原文地址:https://blog.csdn.net/weixin_42938619/article/details/136550800