[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

创建 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

一般的解决方法有两种:

  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;

这样 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 提供的自动提示如下:

在这里插入图片描述

相关推荐

  1. React + Echarts 封装使用

    2024-03-14 22:12:04       33 阅读
  2. react成分(HOC

    2024-03-14 22:12:04       62 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-03-14 22:12:04       75 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-03-14 22:12:04       80 阅读
  3. 在Django里面运行非项目文件

    2024-03-14 22:12:04       64 阅读
  4. Python语言-面向对象

    2024-03-14 22:12:04       75 阅读

热门阅读

  1. week07day03(power bi dax公式 )

    2024-03-14 22:12:04       38 阅读
  2. C#加密和解密、哈希

    2024-03-14 22:12:04       34 阅读
  3. SSE协议介绍

    2024-03-14 22:12:04       46 阅读
  4. 2024.3.13每日一题

    2024-03-14 22:12:04       36 阅读
  5. 04 数据结构之队列

    2024-03-14 22:12:04       40 阅读
  6. STM32day2

    STM32day2

    2024-03-14 22:12:04      32 阅读
  7. adb 筛选查看Unity日志

    2024-03-14 22:12:04       36 阅读
  8. 前端面试练习24.3.12

    2024-03-14 22:12:04       36 阅读