关于typescript:使用React-hooks-的context与reducer搭建数据层

7次阅读

共计 11283 个字符,预计需要花费 29 分钟才能阅读完成。

思来想去很久我决定在新我的项目中应用 context 与 reducer 来搭建数据层,以前我是 Redux 的忠诚拥护者,在一些我的项目中我甚至把所有的 state 放在 redux 当中进行保护,为的就是寻求一种 state 的对立,使数据更加长久化,让整个数据层更加明了;然而在接触 context 与 reducer 的应用之后我决定做出扭转;context 与 reducer 的联合应用切实是太香了,context 与 reducer 的联合应用比 Redux 的流程优化了不少;当然这是 context 与 reducer 的联合应用比照 Redux 的操作流程我得出的论断,然而比照于 React hooks 的其余办法来说 context 与 reducer 仍旧是比拟艰涩难懂的知识点;这也不是必须要学会的知识点(只有面试官不问),因为我感觉光 useState 与 effect 就能解决开发中百分之九十五的问题,如果余下的百分之五须要你多层级的父子组件当中进行数据通信也没必要非要用到 context 与 reducer,因为一层层的组件透传,redux,mobx,都能实现;

言归正传,context 与 reducer 以及他们如何搭配应用;我从 Reducer 开始讲起;

Reducer

首先咱们先明确一个概念什么是 Reducer,仔细的你可能曾经发现 Redux 当中也有 Reducer 这个概念;而 Reducer 的中文翻译是还原剂的意思,我到当初都没明确为什么要这样子命名(想骂鬼佬的取名),所以我把它当成一个概念来了解,一句话就是接管一个初始值与扭转这个初始值的办法,并返回一个 state 与一个和 state 配套的 dispatch 办法,而接管的这个办法就是 reducer;官网给出的解释是:

useState 的代替计划。它接管一个形如 (state, action) => newState 的 reducer,并返回以后的 state 以及与其配套的 dispatch 办法。(如果你相熟 Redux 的话,就曾经晓得它如何工作了。)

在某些场景下,useReducer 会比 useState 更实用,例如 state 逻辑较简单且蕴含多个子值,或者下一个 state 依赖于之前的 state 等。并且,应用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你能够向子组件传递 dispatch 而不是回调函数。

如果你相熟 Vue 你会发现这个 hooks 和 computed 很像,都是为了解决一些单数据或者多数据的属性计算;

那么如何来应用 Reducer?首先咱们须要构想一个简略的应用场景:

某天下午六点当你高高兴兴的拾掇好货色筹备和去女友约会,一位不怀好意的独身的产品经理出于嫉妒的目标让你在页面中加一个计数器性能(程序员怎么可能会有女朋友),此时的你会抉择怎么做?打这个产品经理一顿出气?还是老老实实的实现以下代码?(法制社会,当然选写代码)

import  react,  {useReducer}  from 'react'
import styles from './index.less';

export default function IndexPage() {const [count,dispatch] = useReducer((state,action)=>{if (action === 'sub') {return state - 1}else if(action === 'add'){return state +1}else{return state}
  },0)
  return (
    <div>
      <button onClick={()=>{dispatch('add')}}>+</button>
      <button onClick={()=>{dispatch('sub')}}>-</button>
      <h1>{count}</h1>
    </div>
  );
}

不太好懂,我解释下
应用 useReducer,须要在 useReducer 中传入一个 reducer 和初始值,他会返回一个 state(为了不混同,我决定在代码中应用 count 代替)和 dispatch 办法,你能够用这个 dispatch 去扭转这个 state,而后再说下 reducer,也就是 useReducer 接管的第一个参数,它须要传入一个 state 与 action,state 是它要扭转的值,而 action 是扭转这个值的判断条件,代码中是判断 action 是不是 add 如果是 add 则返回 state+1,是 sub 则返回 state-1,如果都不是就摆烂返回原来的 state,而后在 jsx 语法中咱们应用 h1 去包裹咱们须要渲染的 count,并通过两个按钮调用 dispatch 办法并传入 sub 和 add 去扭转这个 count 的值;总的来说不是特地好懂,倡议手敲两遍看看成果而后再联合文字很快你就明确它的原理了,reducer 是一个不是很说的分明,然而一做就明确的办法;

而后咱们再把话题放回到你与产品经理的相持,
当你信念满满的公布这段代码到服务器并向产品经理夸耀你那温顺可人的女友时,此时你们独身四十年还秃顶的前端主管正站在你的身后看着你的代码,头上冒着青筋手里的饮料瓶被他捏的吱吱响,你意识到问题不对了。这时主管发话了,“你就是这样子写计数器的?!这么简略的代码也须要用到 reducer?!”一段灵魂提问之后,你把代码改成了以下这样:

import  react,  {useState}  from 'react'
import styles from './index.less';

export default function IndexPage() {const [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={()=>{setCount(count-1)}}>+</button>
      <button onClick={()=>{setCount(count-1)}}>-</button>
      <h1 className={styles.title}>{count}</h1>
    </div>
  );
}

这两段代码实现的成果是一样的,主管说的没错,对于一些简略的计算逻辑 useState 比 useReducer 更加快捷不便甚至易懂,也使你的代码更加清晰;然而如果是一些简单逻辑比方购物车的卡券组合,银行的汇率计算等等,此时你刚好又在各个组件当中进行各种通信,我会倡议你应用 useReducer,因为你能够通过联合 context 来进行 diapatch 的透传从而扭转你的 state,这样子会使你的代码可读性变得更高,也会防止疯狂的组件间向下传值,使你的数据通信更加明了,从而优化你的数据流;

而后咱们讲讲 reducer 的好搭档 context;

Context

在以往咱们编写 React 我的项目中咱们都是通过 props 在组件中自内向内传值的,这样子其实会造成一个很大的问题如果是多层组件,而你恰好须要从最外层向最内层传值将会变得十分苦楚,而且会让代码变得十分难懂,在这里咱们就须要用到 Context,官网是这样子形容 Context 的

在一个典型的 React 利用中,数据是通过 props 属性自上而下(由父及子)进行传递的,但此种用法对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都须要的。Context 提供了一种在组件之间共享此类值的形式,而不用显式地通过组件树的逐层传递 props。

那么咱们要怎么来应用 Context?咱们持续来构想一个场景:
还是这个晦气的产品经理,当他看到你给的计数器之后并不是很称心,他想要把计数器展现加在页面底部,而底部与计数器显示页面并不是一个组件,而后在你的骂骂咧咧中你实现了以下代码:

import React, {createContext, useContext, useState} from "react";
const CountContext = createContext(null);
// 底部组件
const Bottom = ()=>{let count = useContext(CountContext)
  return <div>{count}</div>
}
// 显示页面
export default ()=>{const [count,setCount] = useState(0)
  return (
    <div>
      <button onClick={()=>setCount(count+1)}>+</button>
      <button onClick={()=>setCount(count-1)}>-</button>
      <CountContext.Provider value={count}>
        <Bottom/>
      </CountContext.Provider>
    </div>
  )
}

我来解释下以上代码,为了让展现更加直观我将展现页面与底部组件写在了同一个页面;首先咱们要先创立一个 Context 用来贮存与派发数据,所以咱们要用到 createContext 这个办法,

const CountContext = createContext(null);

这个办法有一个参数是你须要传输的数据的默认值,因为咱们这里用到的组件当中不须要默认值,并且咱们须要对这个值在展现页面中创立和应用,所以不须要独自设置默认值,因而我将这个值设为 null;创立完 Context 之后,你所创立的 Context 会返回一个 Provider 组件,此时你须要将你须要传值的组件进行包裹;

// 显示页面
export default ()=>{
  // 在此咱们创立一个 state 用于扭转计数器的值
  const [count,setCount] = useState(0)
  return (
    <div>
      <button onClick={()=>setCount(count+1)}>+</button>
      <button onClick={()=>setCount(count-1)}>-</button>
    /**
    应用 useContext 来接管 Context,接管参数为你所创立的 Context,这个 hooks 会返回一个 state 给你,也就是你用 Provider 所传输的参数,文中接管的参数为 count**/
      <CountContext.Provider value={count}>
        <Bottom/>
      </CountContext.Provider>
    </div>
  )
}

当实现 Context 的创立与 count 的存储传输之后咱们须要在子组件当中对 count 进行接管,此时咱们须要在子组件中调用 useCntext 这个 hooks 对 count 进行接管

import React, {createContext, useContext, useState} from "react";
const CountContext = createContext(null);
const Bottom = ()=>{
/*
    应用 useContext 来接管 Context,接管参数为你所创立的 Context,这个 hooks 会返回一个 state 给你,也就是你用 Provider 所传输的参数,文中接管的参数为 count
*/
  let count = useContext(CountContext)
  return <div>{count}</div>
}

此时如果你实现了以上代码,你会发现曾经能够通过 Context 来对子组件进行数据通信了;

Context 与 Reducer 搭建数据层

既然咱们曾经理解了 Context 与 Reducer 的运行,应该也能联想出 Context 与 Reducer 所搭建的数据流的运行流程;
应用 Context 来进行 state 的存储与通信,Reducer 用来返回批改 state 的 dispatch 办法,整套流程上存储派发更新三个步骤都有了;然而咱们要怎么样实现数据层呢?

比方某天那个不懂事的产品经理有一个需要,让你写一个批改用户信息的需要,而用户信息咱们存在于数据流中进行保护;这时候如果咱们须要批改用户信息,让数据在全局进行扭转与渲染则须要应用 Context 与 Reducer 的搭配应用;

为了不便演示我临时不将它拆开封装,等讲完原理我会将它分类拆开封装;首先咱们须要编写一个 Store 文件;

import React, {createContext, useReducer} from "react";

// 创立一个 context
export const StoreContext = createContext({});
// 以下为为了辨别事件所创建的 typesKey
export const UPDATE_NAME = 'UPDATE_NAME';// 更新名字
export const UPDATE_AGE = 'UPDATE_AGE';// 更新年龄
export const UPDATE_SEX = 'UPDATE_SEX';// 更新性别
export const ADD_AGE = 'ADD_AGE';// 年龄加一
export const SUB_AGE = 'SUB_AGE';// 年龄减一
export const TOGGLE_SEX = 'TOGGLE_SEX';// 切换性别
export const UPDATE_ALL_INFO = 'UPDATE_ALL_INFO'// 更新全副信息
// 默认 State
const defaultState = {
    name: '丧彪',
    age: 18,
    sex: '男'
}

const reducer = (state, action) => {
    // 在 reducer 中须要对事件进行判断来达到不同的 state 更新
    if (action.type === UPDATE_NAME) {state.name = action.name} else if (action.type === UPDATE_AGE) {state.age = action.age} else if (action.type === ADD_AGE) {state.age++} else if (action.type === SUB_AGE) {state.age--} else if (action.type === TOGGLE_SEX) {state.sex = state.sex==='男'?'女':'男'} else if (action.type === UPDATE_ALL_INFO) {
        state.name = action.name
        state.age = action.age
        state.sex = action.sex
    }
    return JSON.parse(JSON.stringify(state))
}
export const Store = (props: any) => {
    // 应用 useReducer 来创立须要向下传值的 state 与扭转 state 的 dispatch 办法
    const [state, dispatch] = useReducer(reducer, defaultState);
    // 在这里咱们须要把 dispatch 也传给子组件,使子组件领有跟新性能
    return (<StoreContext.Provider value={{state, dispatch}}>
            {props.children}
        </StoreContext.Provider>
    )
}

因为曾经讲了 reducer 和 context 的创立与应用流程,这段代码曾经难不倒大家了,所以我只是简略的在代码中进行留神点编注;须要留神的是 defaultState 是一个对象,所以当咱们应用 useReducer 之后所生成的 state 是一个援用类型,所以 react 并不能很好的监听数据变动来进行 render 与 commit,所以须要在 reducer 中每次返回一个新对象,因而我应用简略的 JSON 数据类型转化来生成一个新对象返回;

实现以上代码咱们须要创立两个组件,一个组件用来展现咱们的数据,另一个组件用来扭转咱们的数据;

展现组件

import React,{useContext} from "react";
import {StoreContext} from "@/store";
export default ()=>{const {state} = useContext(StoreContext);
    return (<div style={{background:'blue'}}>
            用户姓名是:{state.name}
            <br/>
            用户性别是:{state.sex}
            <br/>
            用户年龄是:{state.age}
        </div>
    )
}

更新组件

import React, {useContext} from "react";
import {
    StoreContext,
    UPDATE_NAME,
    UPDATE_AGE,
    UPDATE_SEX,
    ADD_AGE,
    SUB_AGE,
    TOGGLE_SEX
} from "@/store";

export default () => {const {dispatch} = useContext(StoreContext);
    let [name, age, sex] = [null, null, null]

    return (
        <div>
            更新姓名 <input type="text" onInput={e => {name = e.target.value}}/>
            <button onClick={()=>dispatch({type:UPDATE_NAME,name})}>update</button>
            <br/>
            <br/>
            <br/>
            性别切换:
            <button onClick={()=>dispatch({type:TOGGLE_SEX})}>toggle</button>
            <br/>
            <br/>
            <br/>
            更新性别 <input type="text" onInput={e => {sex = e.target.value}}/>
            <button onClick={()=>dispatch({type:UPDATE_SEX,sex})}>update</button>
            <br/>
            <br/>
            <br/>
            更新年龄: <input type="text" onInput={e => {age = e.target.value}}/>
            <button onClick={()=>dispatch({type:UPDATE_AGE,age})}>update</button>
            <br/>
            <br/>
            <br/>
            年龄 +1: <button onClick={()=>dispatch({type:ADD_AGE})}>add</button>
            <br/>
            <br/>
            <br/>
            更新 -1: <button onClick={()=>dispatch({type:SUB_AGE})}>sub</button>

        </div>
    )
}

再而后咱们在入口文件中引入咱们所写的 Store,展现组件和更新组件;并用 Store 对展现组件和更新组件进行包裹向下传值

import  react from 'react'
import styles from './index.less';
import {Store} from "@/store";
import Preview from '@/pages/Preview'
import Update from '@/pages/Update'
export default function IndexPage() {

  return (
      <Store>
          <Preview/>
          <Update/>
      </Store>
  );
}

以上咱们便实现了数据层存储,传值,批改的整个过程;

然而如果是构建企业级的数据层咱们的 store 尽管曾经实现了整个周期,然而因为把所有文件都写在一起使代码可读性变得很差,所以在这里咱们须要对 store 进行拆分封装,拆分封装次要是依据集体习惯,以及是否适宜我的项目来进行拆分;以下我举一个例子,我将其拆分为:

  1. actionTypes.ts 用来封装 Reducer 判断数据跟新形式的 key;
  2. actions.ts 用来封装扭转数据与数据初始化的 Reducer 返回值办法;
  3. state.ts 用来封装默认 state;
  4. index.tsx store 入口文件,用来裸露 Types 与创立的 Context Provider 组件;

actionTypes.ts 用来封装 Reducer 判断数据跟新形式的 key

export const UPDATE_NAME = 'UPDATE_NAME';// 更新名字
export const UPDATE_AGE = 'UPDATE_AGE';// 更新年龄
export const UPDATE_SEX = 'UPDATE_SEX';// 更新性别
export const ADD_AGE = 'ADD_AGE';// 年龄加一
export const SUB_AGE = 'SUB_AGE';// 年龄减一
export const TOGGLE_SEX = 'TOGGLE_SEX';// 切换性别
export const UPDATE_ALL_INFO = 'UPDATE_ALL_INFO'// 更新全副信息

actions.ts 用来封装扭转数据与数据初始化的 Reducer 返回值办法

import  * as types from './actionTypes'
export default {[types.UPDATE_NAME](state,actions){
        state.name = actions.name
        return state
    },
    [types.UPDATE_AGE](state,actions){
        state.age = actions.age
        return state
    },
    [types.UPDATE_SEX](state,actions){
        state.sex = actions.sex
        return state
    },
    [types.ADD_AGE](state){
        state.age++
        return state
    },
    [types.SUB_AGE](state){
        state.age--
        return state
    },
    [types.TOGGLE_SEX](state){
        state.sex =  state.sex==='男'?'女':'男'
        return state
    },
}

state.ts 用来封装默认 state

export default  {
    name: '丧彪',
    age: 18,
    sex: '男'
}

index.tsx store 入口文件,用来裸露 Types 与创立的 Context Provider 组件

import React, {createContext, useReducer} from "react";
import defaultState from './state'
import * as Types from './actionTypes'
import actions from './actions'
export const types = Types
export const StoreContext = createContext({});

const reducer = (state, action) => {return actions[action.type]?JSON.parse(JSON.stringify(actions[action.type](state,action))):state
}
export const Store = (props: any) => {const [state, dispatch] = useReducer(reducer, defaultState);
    return (<StoreContext.Provider value={{state, dispatch}}>
            {props.children}
        </StoreContext.Provider>
    )
}

以下为封装好的数据层在页面中援用:

  1. Priview.tsx 展现组件;
  2. Update.tsx 更新组件;
  3. Index.tsx 展现组件与更新组件的父组件;

Priview.tsx 展现组件;

import React,{useContext} from "react";
import {StoreContext} from "@/store";
export default ()=>{const {state} = useContext(StoreContext);
    return (<div style={{background:'blue'}}>
            用户姓名是:{state.name}
            <br/>
            用户性别是:{state.sex}
            <br/>
            用户年龄是:{state.age}
        </div>
    )
}

Update.tsx 更新组件

import React, {useContext} from "react";
import {
    StoreContext,
    types
} from "@/store";

export default () => {const {dispatch} = useContext(StoreContext);
    let [name, age, sex] = [null, null, null]

    return (
        <div>
            更新姓名 <input type="text" onInput={e => {name = e.target.value}}/>
            <button onClick={()=>dispatch({type:types.UPDATE_NAME,name})}>update</button>
            <br/>
            <br/>
            <br/>
            性别切换:
            <button onClick={()=>dispatch({type:types.TOGGLE_SEX})}>toggle</button>
            <br/>
            <br/>
            <br/>
            更新性别 <input type="text" onInput={e => {sex = e.target.value}}/>
            <button onClick={()=>dispatch({type:types.UPDATE_SEX,sex})}>update</button>
            <br/>
            <br/>
            <br/>
            更新年龄: <input type="text" onInput={e => {age = e.target.value}}/>
            <button onClick={()=>dispatch({type:types.UPDATE_AGE,age})}>update</button>
            <br/>
            <br/>
            <br/>
            年龄 +1: <button onClick={()=>dispatch({type:types.ADD_AGE})}>add</button>
            <br/>
            <br/>
            <br/>
            更新 -1: <button onClick={()=>dispatch({type:types.SUB_AGE})}>sub</button>

        </div>
    )
}

Index.tsx 展现组件与更新组件的父组件

import  react from 'react'
import styles from './index.less';
import {Store} from "@/store";
import Preview from '@/pages/Preview'
import Update from '@/pages/Update'
export default function IndexPage() {

  return (
      <Store>
          <Preview/>
          <Update/>
      </Store>
  );
}

其余

context 与 reducer 的封装差不多就这点货色,如果想晓得整个运行流程请参照 reducer 与 context 搭配应用的未拆分流程;这里说一句每次调用 dispatch 的时候其实都会返回一个新的 state(蕴含全副数据的对象), 因而在这里我拿简略的解决深拷贝的形式来解决了下,集体不太喜爱这种形式,因为对性能有损耗,然而如果你谋求整个我的项目的性能,其实能够用到一些第三方库,比方 immerjs 或者 immutablejs 来搭建一个长久化数据层,如果你这样子做了,那么对我的项目的内存读取以及避免内存溢出下面会有较大晋升,然而这些扭转是肉眼无奈轻易觉察的,如果你的我的项目是短暂的巨型的那么我倡议你应用 immerjs 与 immutablejs;

下一期讲讲 immerjs 以及以及简略的搭建长久化数据层;

正文完
 0