前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >在爱 context 一次,并结合 useReducer 使用,这次有一点简单

在爱 context 一次,并结合 useReducer 使用,这次有一点简单

作者头像
用户6901603
发布2023-12-20 16:17:21
1470
发布2023-12-20 16:17:21
举报
文章被收录于专栏:不知非攻不知非攻

React 知命境」第 28 篇

在 React 中,props 能够帮助我们将数据层层往下传递。而 context 能够帮助我们将数据跨层级往下传递

context 的概念稍微有一点点多,但是我们在学习的他的时候,只需要将其分为两个部分,就能够轻松掌握

  • 1、如何创建 context 与如何传递数据
  • 2、组件中如何获取数据

context 如何创建与数据如何传递

react 中使用 createContext组件外部创建 context

代码语言:javascript
复制
const context = createContext(defaultValue)

context 本身不保存任何信息,他包含了两个引用

context.Provider 用于包裹子组件并传递数据

context.Consumer 用于在子组件中读取数据,不过这个读取方式已经非常少能有用武之地了,基本上都被 useContext 取代了。

一个非常简单的 demo 如下。首先我们一定要明确的把 Provider 当成顶层父组件,因为我们的目标就是把数据从父组件往更低层的子组件传递,因此我们首先要创建父组件

代码语言:javascript
复制
import { createContext } from 'react';  

const ThemeContext = createContext('light');

function App() {
  const [theme, setTheme] = useState('light');
  // ...
  return (
    <ThemeContext.Provider value={theme}>
      <Page />
    </ThemeContext.Provider>
  );
}

Provider 通过 value 将定义好的数据传递下去。在子组件 Page 以及他更低层的子组件中,我们都可以使用 useContext 来获取数据

数据如何获取

假如在上面案例的子组件 Page 内部,还有一个更底层次的子组件 Button , 在 Button 中,我们可以通过 useContext 这个 hook 来获取从顶层父组件传递过来的参数

代码语言:javascript
复制
function Button() {
  // ✅ Recommended way
  const theme = useContext(ThemeContext);
  return <button className={theme} />;
}

当然,在以前我们也可以通过 Consumer 来获取,不过现在已经不推荐这样使用了

代码语言:javascript
复制
function Button() {
  // 🟡 Legacy way (not recommended)
  return (
    <ThemeContext.Consumer>
      {theme => (
        <button className={theme} />
      )}
    </ThemeContext.Consumer>
  );
}

支持嵌套

多个 context 可以嵌套使用

代码语言:javascript
复制
import { createContext } from 'react';

const ThemeContext = createContext('light');
const AuthContext = createContext(null);

function App() {
  const [theme, setTheme] = useState('dark');
  const [currentUser, setCurrentUser] = useState({ name: 'Taylor' });

  // ...

  return (
    <ThemeContext.Provider value={theme}>
      <AuthContext.Provider value={currentUser}>
        <Page />
      </AuthContext.Provider>
    </ThemeContext.Provider>
  );
}

结合 TS 使用

我们要结合 TS 来实现一个案例,在子组件中有两个按钮,他们分别可以对数字进行递增或者递减操作。首先我们简单调整一下实现思路,封装一个顶层父组件,并在该父组件中约定好数据和操作数据的方法。接收子组件为参数

先使用 interface 约定好数据的类型

代码语言:javascript
复制
interface Injected {
  counter: number,
  setCounter: Dispatch<any>,
  increment: () => any,
  decrement: () => any
}

顺带简单定义一下 props 的类型,目前只接收一个 children 作为参数

代码语言:javascript
复制
interface Props {
  children?: any
}

然后创建的 context,createContext 接收刚才约定好的类型作为泛型传入

代码语言:javascript
复制
export const context = createContext<Injected>({} as Injected)

准备工作做好了之后,接下来约定好数据即可,组件代码如下

代码语言:javascript
复制
export function CounterProvider({children}: Props) {
  const [counter, setCounter] = useState(0)

  const value = {
    counter,
    setCounter,
    increment: () => setCounter(counter + 1),
    decrement: () => setCounter(counter - 1)
  }

  return (
    <context.Provider value={value}>
      {children}
    </context.Provider>
  )
}

顶层父组件封装好之后,我们只需要将子组件封装好,然后组合起来即可

代码语言:javascript
复制
export default () => (
  <CounterProvider>
    <Demo />
  </CounterProvider>
)

在子组件中,使用 useContext 获取数据和操作数据的方法

代码语言:javascript
复制
import {useContext} from 'react'
import Button from 'src/components/Button'
import {context, CounterProvider} from './CounterProvider'

function Demo() {
  const {counter, increment, decrement} = useContext(context)
  return (
    <div>
      <div>{counter}</div>
      <Button onClick={increment}>递增</Button>
      <Button onClick={decrement}>递减</Button>
    </div>
  )
}

改造:结合 useReducer 来使用

一些团队或者开源项目,会基于 context 和 useReducer 来封装状态管理,用来替代 redux 在项目中的地位。这是一个非常不错的想法。现在我们把上面一个案例稍微改造一下,也来试试。

案例目录结构如下,index.tsx 为项目的入口,Counter 表示子组件,Provider 表示顶层父组件

代码语言:javascript
复制
+ App
  - index.tsx
  - Provider.tsx
  - Counter.tsx

假如项目的子组件和顶层父组件都已经封装好了,那么在入口文件中的代表应该为

代码语言:javascript
复制
import {Provider} from './Provider'
import Counter from './Counter'

export default function App() {
  return (
    <Provider>
      <Counter />
    </Provider>
  )
}

我们接下来先思考一下顶层的 Provider 组件应该如何封装。

首先,我们需要先约定好 state 的类型,该案例中,只有一个数字,因此类型定义为

代码语言:javascript
复制
interface State {
  counter: number
}

context 要往底层组件中传递修改数据的方式,因此还需要定义另外一个 context 的类型

代码语言:javascript
复制
interface Injected extends State {
  increment: () => any,
  decrement: () => any
}

然后做一些其他的简单类型约定

代码语言:javascript
复制
interface Props {
  children?: any
}

type Action = {
  type: string,
  [key: string]: any
}

定义好初始状态

代码语言:javascript
复制
const initialState = { counter: 0 }

定义 context

代码语言:javascript
复制
export const context = createContext<Injected>(initialState as Injected)

定义好 reducer,这里需要特别注意的是 reducer 的类型一定要约定好

代码语言:javascript
复制
const reducer: Reducer<State, Action> = (state, action) => {
  if (action.type == 'increment') {
    return {
      counter: state.counter + 1
    }
  }

  if (action.type == 'decrement') {
    return {
      counter: state.counter - 1
    }
  }
  return state
}

准备工作都做好了之后,我们再定义 Provider 组件

代码语言:javascript
复制
export function Provider({children}: Props) {
  const [state, dispatch] = useReducer(reducer, initialState)

  const value = {
    counter: state.counter,
    increment: () => dispatch({ type: 'increment' }),
    decrement: () => dispatch({ type: 'decrement' }),
  }

  return (
    <context.Provider value={value}>
      {children}
    </context.Provider>
  )
}

这样,顶层父组件就搞定了。剩下的就是封装子组件。子组件只要包裹在我们封装好的 Provider 之下,我们就可以在子组件中通过 useContext 轻松获取状态,代码如下

代码语言:javascript
复制
import {useContext} from 'react'
import Button from 'src/components/Button'
import {context} from './Provider'

export default function Counter() {
  const {counter, increment, decrement} = useContext(context)
  return (
    <div>
      <div>{counter}</div>
      <Button onClick={increment}>递增</Button>
      <Button onClick={decrement}>递减</Button>
    </div>
  )
}

大功告成。惊喜的是,在逻辑清晰的情况下,我们发现 useReducer + useContext 使用起来也不是很困难。

我们在来一个更复杂一点的案例,巩固一下我们学习到的知识。

更复杂的案例

需求是实现一个任务列表。

  • 1、 列表中的每一项都可以被删除
  • 2、 列表中的每一项都可以编辑
  • 3、 可以新增列表

思考一下之后,我决定把列表单独封装在一个子组件里,新增列表的操作封装在另外一个子组件里,然后使用 Provider 把他们包裹起来,项目的结果如下

代码语言:javascript
复制
+ App
  - index.tsx
  - Provider.tsx
  - TaskList.tsx
  - AddTask.tsx

在封装 Provider 时,我们可以把在内部基于 useContext 封装一些自定义 hooks,来简化子组件的操作

代码语言:javascript
复制
export function useTasks() {
  return useContext(TasksContext)
}

export function useDispatch() {
  return useContext(DispatchContext)
}

先约定好一些前置的类型声明

代码语言:javascript
复制
export type Task = {
  id: number,
  text: string,
  done: boolean
}

interface Props {
  children?: any
}

export type Action = {
  type: string,
  [key: string]: any
}

约定初始化数据

代码语言:javascript
复制
const initialTasks = [
  { id: 0, text: 'Philosopher’s Path', done: true },
  { id: 1, text: 'Visit the temple', done: false },
  { id: 2, text: 'Drink matcha', done: false }
];

定义两个 context。分别用于传递数据和操作数据的方法。这里只是为了增加语法说明新增的操作方式,实践中不必非要如此

代码语言:javascript
复制
export const TasksContext = createContext(initialTasks)
const DispatchContext = createContext<Dispatch<Action>>(null as any)

定义好 reducer 函数

代码语言:javascript
复制
const reducer: Reducer<Task[], Action> = (tasks, action) => {
  if (action.type == 'added') {
    return [
      ...tasks, {
        id: action.id,
        text: action.text,
        done: false
      }
    ]
  }

  if (action.type == 'changed') {
    return tasks.map(t => {
      if (t.id == action.task.id) {
        return action.task
      }
      return t
    })
  }

  if (action.type == 'deleted') {
    return tasks.filter(t => t.id !== action.id)
  }

  return tasks
}

最后定义 Provider

代码语言:javascript
复制
export function Provider({children}: Props) {
  const [tasks, dispatch] = useReducer(reducer, initialTasks)

  return (
    <TasksContext.Provider value={tasks}>
      <DispatchContext.Provider value={dispatch}>
        {children}
      </DispatchContext.Provider>
    </TasksContext.Provider>
  )
}

export function useTasks() {
  return useContext(TasksContext)
}

export function useDispatch() {
  return useContext(DispatchContext)
}

子组件的逻辑就比较简单了,只需要通过自定义的 useTasksuseDispatch 获取数据和对应的操作即可。

代码语言:javascript
复制
// AddTask.tsx
import { useState } from 'react';
import { useDispatch } from './Provider';

export default function AddTask() {
  const [text, setText] = useState('');
  const dispatch = useDispatch();
  return (
    <>
      <input
        placeholder="Add task"
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <button onClick={() => {
        setText('');
        dispatch({
          type: 'added',
          id: nextId++,
          text: text,
        }); 
      }}>Add</button>
    </>
  );
}

let nextId = 3;

代码语言:javascript
复制
// TaskList.tsx
import { useState } from 'react';
import { useTasks, useDispatch, Task } from './Provider';

export default function TaskList() {
  const tasks = useTasks();
  return (
    <ul>
      {tasks.map(task => (
        <li key={task.id}>
          <TaskItem task={task} />
        </li>
      ))}
    </ul>
  );
}

function TaskItem({ task }: { task: Task }) {
  const [isEditing, setIsEditing] = useState(false);
  const dispatch = useDispatch();
  let taskContent;
  if (isEditing) {
    taskContent = (
      <>
        <input
          value={task.text}
          onChange={e => {
            dispatch({
              type: 'changed',
              task: {
                ...task,
                text: e.target.value
              }
            });
          }} />
        <button onClick={() => setIsEditing(false)}>
          Save
        </button>
      </>
    );
  } else {
    taskContent = (
      <>
        {task.text}
        <button onClick={() => setIsEditing(true)}>
          Edit
        </button>
      </>
    );
  }
  return (
    <label>
      <input
        type="checkbox"
        checked={task.done}
        onChange={e => {
          dispatch({
            type: 'changed',
            task: {
              ...task,
              done: e.target.checked
            }
          });
        }}
      />
      {taskContent}
      <button onClick={() => {
        dispatch({
          type: 'deleted',
          id: task.id
        });
      }}>
        Delete
      </button>
    </label>
  );
}

子组件和顶层父组件都封装好之后,我们只需要在 App.tsx 中把他们组合起来就可以了

代码语言:javascript
复制
import AddTask from './AddTask';
import TaskList from './TaskList';
import { Provider } from './Provider';

export default function TaskApp() {
  return (
    <Provider>
      <h1>Day off in Kyoto</h1>
      <AddTask />
      <TaskList />
    </Provider>
  );
}

OK,搞定。虽然这个例子从交互上变得更加复杂了,但是理解起来的难度并没有任何增加。基于这套逻辑,稍微扩展丰富一下,你就能开发出来一个自己的状态管理器。

不过,也别高兴得太早,关于 context,还有一些东西需要我们去攻克,他跟性能优化有关,我们在后续的文章中,继续学习。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-12-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 这波能反杀 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • context 如何创建与数据如何传递
  • 数据如何获取
  • 支持嵌套
  • 结合 TS 使用
  • 改造:结合 useReducer 来使用
  • 更复杂的案例
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档