likes
comments
collection
share

成都的都江堰,React 的上下文

作者站长头像
站长
· 阅读数 31

React 上下文

前言

说起React上下文,我想说说都江堰,余秋雨先生在游览都江堰后写到“拜水都江堰,问道青城山”,为啥拜水,那是因为李冰父子为了解决水患,达到了分江引流的作用。正所谓弱水三千只取一瓢,只用自己能用的,不是自己的也不惦记,我们来看看React上下文,看看是不是跟都江堰异曲同工。

前面我们已经掌握了如何通过属性在组件树中向上或向下传递状态,然后如果组件树层级太深,是不是我们就得传递很多次,这就好比我们坐火车,每到一个站就要转车,所以我们迫切的需要一趟直达车来解决面临的尴尬,而且这样一来,功能就变得复杂化,且不便于维护容易出错,所以我们今天来探讨。

React 提供了 一个 createContext 函数来创建上下文对象,其中包括一个供应组件 Provider 和一个消费组件 Consumer,我们具体来看一下这两个组件该怎么来使用,我们还是用之前的周计划代码来进行改造。我们具体来看一下利用 React 上下文来创建的组件,是什么样?

使用上下文供应组件与消费组件

若果我们把供应组件 Provider 放在组件树的顶端,那么组件树下任一组件都可以用消费组件 Consumer 进行消费。

1. 创建数据源文件 weekPlanList.json 文件

[
  {
    "title": "周一的计划",
    "index": "1",
    "todoItems": [
      {
        "index": "1-1",
        "time": "7:00 PM",
        "matters": "运动健身"
      },
      {
        "index": "1-2",
        "time": "9:00 PM",
        "matters": "阅读"
      }
    ]
  },
  {
    "title": "周二的计划",
    "index": "2",
    "todoItems": [
      {
        "index": "2-1",
        "time": "7:00 PM",
        "matters": "练习萨克斯"
      },
      {
        "index": "2-2",
        "time": "8:00 PM",
        "matters": "看一部电影"
      }
    ]
  },
  {
    "title": "周三的计划",
    "index": "3",
    "todoItems": [
      {
        "index": "3-1",
        "time": "7:00 PM",
        "matters": "练习毛笔字"
      },
      {
        "index": "3-2",
        "time": "8:00 PM",
        "matters": "短距离夜骑"
      }
    ]
  },
  {
    "title": "周四的计划",
    "index": "4",
    "todoItems": [
      {
        "index": "4-1",
        "time": "7:00 PM",
        "matters": "运动健身"
      },
      {
        "index": "4-2",
        "time": "8:00 PM",
        "matters": "高数学习"
      }
    ]
  },
  {
    "title": "周五的计划",
    "index": "5",
    "todoItems": [
      {
        "index": "5-1",
        "time": "7:00 PM",
        "matters": "云顶之奕"
      }
    ]
  },
  {
    "title": "周六的计划",
    "index": "6",
    "todoItems": [
      {
        "index": "6-1",
        "time": "7:00 AM",
        "matters": "绿道骑行"
      },
      {
        "index": "6-2",
        "time": "7:00 PM",
        "matters": "线代学习"
      }
    ]
  },
  {
    "title": "周日的计划",
    "index": "7",
    "todoItems": [
      {
        "index": "7-1",
        "time": "8:00 AM",
        "matters": "学习,整理,总结"
      },
      {
        "index": "7-2",
        "time": "3:00 PM",
        "matters": "追番,追剧,追综艺"
      }
    ]
  }
]

2. 创建上下文供应组件 Provider App.js 组件

import { createContext, useState } from 'react';
import WeekList from './components/WeekList';
import weekplanList from './source/weekPlanList.json';

const weekPlans = [...weekplanList];

export const CheckedContext = createContext();

function App() {
  const [checkedIndexList, setCheckedIndexList] = useState([]);

  const checkedChange = (val) => {
    let checkedList = checkedIndexList.filter(checkedIndex => checkedIndex !== val);
    if (checkedList.length === checkedIndexList.length) {
      checkedList = [...checkedList, val];
    }

    setCheckedIndexList(checkedList);
  }
  return (
    <div className="App">
      <div className="plan-head">周计划管理</div>
      <div className="plan-body">
        <CheckedContext.Provider value={{checkedIndexList, checkedChange}}>
          <WeekList weekList={weekPlans}></WeekList>
        </CheckedContext.Provider>
      </div>
    </div>
  );
}

export default App;

我们在组件树的顶端引入上下文,用 createContext 钩子来创建上下文 CheckedContext,注意我们需要把上下文导出,以便子组件引用,再将子组件包裹在供应组件 CheckedContext.Provider 里,我们只需要将数据,方法以属性的形式来进行传递,需要用到该数据与方法的子组件,引入消费组件进行消费就可以了,就像是一条河流,下游需要用水,无论在哪里,只需要接入一个支流就可以引水了,道理就是这个道理,应该还是比较好理解。

3. 创建 WeekList.js 组件

/**
 * WeekList.js
 */

import React from 'react';
import TodoItems from "./TodoItems";

function WeekList({weekList}) {
  return weekList.map((week, index) => (
    <React.Fragment key={index}>
      <div className="week-title">{ week.title }</div>
      <div className="items-container">
        <TodoItems {...week} />
      </div>
    </React.Fragment>
  ))
} 

export default WeekList;

可以看到,这一层级组件没有用到上下文的东西,也没有向下传递 checkedIndexList 的属性与方法,也就是上下文的出现让我们在开发较多层级组件时不用再一层一层的把数据给传递到末端,让组件更纯粹,开发也更轻松。

4. 创建 TodoItems.js 组件

/**
 * TodoItems.js
 * 每天待处理项列表
 * @param {*} param0 
 * @returns 
 */
import { CheckedContext } from "../App";
import TodoItem from "./TodoItem";

function TodoItems ({todoItems}) {
  return (
    <CheckedContext.Consumer>
      { context => (
        <ul>
          {todoItems.map(item => (
            <li key={item.index} >
              <TodoItem {...item} checked={context.checkedIndexList.includes(item.index)} />
            </li>
          ))}
        </ul>
      )
      }
    </CheckedContext.Consumer>
  )
}

export default TodoItems;

引入山下文,用消费组件 CheckedContext.Consumer 对上下文中的数据进行消费,注意我们在使用消费组件时,使用了一种称为"渲染属性"的模式来获取上下文对象 context,通过上下文对象获取对应的数据,再进行消费。

5. 创建 TodoItem.js 组件


/**
 * TodoItem.js
 * 每天待处理项
 * @param {*} param0 
 * @returns 
 */
import {CheckedContext} from '../App';


function TodoItem ({matters, time, checked, index}) {
  return(
    <CheckedContext.Consumer>
      {context => (
        <div className="check-item">
          <label htmlFor="input">{ `${time} : ${matters}` }</label>
          <input
            type="checkbox"
            checked={checked}
            value={index}
            style={{marginLeft: '6px'}}
            onChange={
              (e) => {
                context.checkedChange(e.target.value);
              }
            }></input>
        </div>
      )}
    </CheckedContext.Consumer>
  )
}

export default TodoItem;

到现在为止,我们已经把上下文的创建,组件供应,数据传递,组件消费这四大环节介绍完成,但是在上述消费组件内,我们以"渲染属性"模式来获取 checkedChange 方法,难免有些麻烦,且可读性不强。

使用 useContext 来获取上下文对象

为了处理"渲染属性"模式带来的烦恼,React 提供了 useContext 钩子来获取相应的上下文对象。我们现在对 TodoItems.js 组件和 TodoItem.js 组件稍加修改。

TodoItems.js 组件:

/**
 * TodoItems.js
 * 每天待处理项列表
 * @param {*} param0 
 * @returns 
 */
import TodoItem from "./TodoItem";
import { CheckedContext } from "../App";
import { useContext } from "react";

function TodoItems ({todoItems}) {
  const { checkedIndexList } = useContext(CheckedContext);
  
  return (
    <ul>
      {todoItems.map(item => (
        <li key={item.index} >
          <TodoItem {...item} checked={checkedIndexList.includes(item.index)} />
        </li>
      ))}
    </ul>
  )
}

export default TodoItems;

TodoItem.js 组件:


/**
 * TodoItem.js
 * 每天待处理项
 * @param {*} param0 
 * @returns 
 */
import { CheckedContext } from '../App';
import { useContext } from 'react';

function TodoItem ({ matters, time, checked, index }) {
  const { checkedChange } = useContext(CheckedContext);
  
  return(
    <div className="check-item">
      <label htmlFor="input">{ `${time} : ${matters}` }</label>
      <input
        type="checkbox"
        checked={checked}
        value={index}
        style={{marginLeft: '6px'}}
        onChange={(e) => { checkedChange(e.target.value); }}
      />
    </div>
  )
}

export default TodoItem;

useContext 钩子用于从上下文 Consumer 中获取我们需要的值,而不再从 "渲染属性" 模式来获取,加上钩子的辅助,这样使用上下文简直让人赏心悦目。

自定义上下文钩子

在上下文中,我们既要创建,又要分发,又要使用,分布在不同的组件中,这样的管理是否会让你感到有些繁琐,要是这些操作都能集中起来一起处理就好了。我们来尝试把上下文封装在一个自定义的钩子中。

我们创建一个 checked-hooks.js 文件:

import { createContext, useContext, useState } from "react";

const checkedContext = createContext();

export const CheckedProvider = ({children}) => {
  const [checkedIndexList, setCheckedIndexList] = useState([]);
  const checkedChange = (val) => {
    let checkedList = checkedIndexList.filter(checkedIndex => checkedIndex !== val);
    if (checkedList.length === checkedIndexList.length) {
      checkedList = [...checkedList, val];
    }
  
    setCheckedIndexList(checkedList);
  }
  
  return (
    <checkedContext.Provider value={{checkedIndexList, checkedChange}}>
      {children}
    </checkedContext.Provider>
  )
}

export const useChecked = () => useContext(checkedContext);

我们将状态管理,与上下文供应与消费封装成一个整体,在数据供应时我们使用 CheckedProvider,在消费时我们使用 useChecked 钩子,这样我们把上下文功能属性集中管理,那么在使用时那不就如探囊取物一样简单。

我们再来看看如何使用自定义的山下文钩子:

App.js 组件:

import WeekList from './components/WeekList';
import weekplanList from './source/weekPlanList.json';
import { CheckedProvider } from './components/checked-hooks';

const weekPlans = [...weekplanList];

function App() {
  return (
    <div className="App">
      <div className="plan-head">周计划管理</div>
      <div className="plan-body">
      <CheckedProvider>
        <WeekList weekList={weekPlans}></WeekList>
      </CheckedProvider>
      </div>
    </div>
  );
}

export default App;

TodoItems.js 组件:

/**
 * TodoItems.js
 * 每天待处理项列表
 * @param {*} param0 
 * @returns 
 */
import TodoItem from "./TodoItem";
import { useChecked } from "./checked-hooks";

function TodoItems ({todoItems}) {
  const { checkedIndexList } = useChecked();
  
  return (
    <ul>
      {todoItems.map(item => (
        <li key={item.index} >
          <TodoItem {...item} checked={checkedIndexList.includes(item.index)} />
        </li>
      ))}
    </ul>
  )
}

export default TodoItems;

TodoItem.js 组件:

/**
 * TodoItem.js
 * 每天待处理项
 * @param {*} param0 
 * @returns 
 */
import { useChecked } from './checked-hooks'

function TodoItem ({matters, time, checked, index}) {
  const { checkedChange } = useChecked();
  
  return(
    <div className="check-item">
      <label htmlFor="input">{ `${time} : ${matters}` }</label>
      <input
        type="checkbox"
        checked={checked}
        value={index}
        style={{marginLeft: '6px'}}
        onChange={(e) => { checkedChange(e.target.value); }}
      />
    </div>
  )
}

export default TodoItem;

总结

主要内容:React 中上下文的使用

成都的都江堰,React 的上下文

在本文中,通过一步一步比较细致代码演变,最终封装成一个简单可复用的上下文钩子,主在表达利用学习上下文之余,再尝试引导思考一下钩子的封装。在 React 中我们利用上下文,加上状态管理来达到对于属性灵活管理,使得我们在应用时游刃有余,看着图简单的回顾一下我们这一节的内容:

  1. creatContext:用于创建上下文 context;
  2. context 提供一个供应组件 Provider 和一个 消费组件 Consumer;
  3. 供应组件使得整个组件树任何一级都可以拿到上下文存储的信息
  4. 消费组件可通过“渲染属性”模式和 useContext 钩子获取上下文属性值;
  5. 通过状态管理 useState,createContext,useContext,将上下文通过的属性,方法封装在一个模块中。