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

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

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


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


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


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


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

const context = createContext(defaultValue)

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


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


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


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

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 来获取从顶层父组件传递过来的参数

function Button() {
  // ✅ Recommended way
  const theme = useContext(ThemeContext);
  return <button className={theme} />;
}

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

function Button() {
  // 🟡 Legacy way (not recommended)
  return (
    <ThemeContext.Consumer>
      {theme => (
        <button className={theme} />
      )}
    </ThemeContext.Consumer>
  );
}


支持嵌套


多个 context 可以嵌套使用

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 约定好数据的类型

interface Injected {
  counter: number,
  setCounter: Dispatch<any>,
  increment: () => any,
  decrement: () => any
}

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

interface Props {
  children?: any
}

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

export const context = createContext<Injected>({} as Injected)

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

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>
  )
}

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

export default () => (
  <CounterProvider>
    <Demo />
  </CounterProvider>
)

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

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 表示顶层父组件

+ App
  - index.tsx
  - Provider.tsx
  - Counter.tsx

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

import {Provider} from './Provider'
import Counter from './Counter'
export default function App() {
  return (
    <Provider>
      <Counter />
    </Provider>
  )
}

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


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

interface State {
  counter: number
}

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

interface Injected extends State {
  increment: () => any,
  decrement: () => any
}

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

interface Props {
  children?: any
}
type Action = {
  type: string,
  [key: string]: any
}

定义好初始状态

const initialState = { counter: 0 }

定义 context

export const context = createContext<Injected>(initialState as Injected)

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

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 组件

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 轻松获取状态,代码如下

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 把他们包裹起来,项目的结果如下

+ App
  - index.tsx
  - Provider.tsx
  - TaskList.tsx
  - AddTask.tsx

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

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

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

export type Task = {
  id: number,
  text: string,
  done: boolean
}
interface Props {
  children?: any
}
export type Action = {
  type: string,
  [key: string]: any
}

约定初始化数据

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。分别用于传递数据和操作数据的方法。这里只是为了增加语法说明新增的操作方式,实践中不必非要如此

export const TasksContext = createContext(initialTasks)
const DispatchContext = createContext<Dispatch<Action>>(null as any)

定义好 reducer 函数

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

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)
}

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

// 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;
// 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 中把他们组合起来就可以了

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,还有一些东西需要我们去攻克,他跟性能优化有关,我们在后续的文章中,继续学习。

相关文章
|
2月前
|
前端开发 开发者
useContext 钩子详解
【10月更文挑战第14天】`useContext` 是 React 中的一个 Hook,用于在组件树中传递数据,避免手动传递 props。本文从基本概念、使用方法、常见问题及解决方法等方面详细介绍了 `useContext`,并提供了代码示例,帮助开发者更好地理解和应用这一钩子。
80 5
|
3月前
|
前端开发
React属性之context属性
React中的Context属性用于跨组件传递数据,通过Provider和Consumer组件实现数据的提供和消费。
39 3
|
4月前
|
前端开发 API
React 中 Context 的概念
【8月更文挑战第31天】
54 0
|
7月前
|
前端开发
深入理解 React 中的 Context
深入理解 React 中的 Context
254 1
|
7月前
|
前端开发
深入理解 React 中的 Context(二)useContext、createContext
深入理解 React 中的 Context(二)useContext、createContext
266 0
|
前端开发
React的context传值方法介绍
1.在src在创建一个context.js文件
React的context传值方法介绍
|
前端开发
React Context是什么
React Context是什么
|
JavaScript
vue v-bind=“$attrs“、v-on=“$listeners“
vue v-bind=“$attrs“、v-on=“$listeners“
156 0
|
前端开发
在React如何使用Context
React中的Context是一种用于在组件树中传递数据的方法。它可以帮助我们在React应用中进行全局状态管理。本文将介绍Context的基本用法,并演示如何在React中使用Context进行全局状态管理。
133 0
|
前端开发
react 进阶hook 之 context Hook
react context 上下文,相信大家都会使用。不会的可查看。详情的概念这里不介绍,只接受在函数组件中,使用context hooks 的使用
react 进阶hook 之 context Hook