React Hooks + Context打造简易redux

HookReact 16.8的新特性,它可以让在不编写class类组件的情况下使用state以及其他的React特性;而ContextReact16.3版本里面引入新的Context API,在以往React版本中存在一个Context API,那是一个幕后试验性功能,官方提议避免使用,Redux的原理就是建立在旧的Context API。现在新的Context ApI提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法,为数据通讯另辟蹊径。

Context简介

为解决多层嵌套不同层级组件之间props数据传递,这种数据传递及其繁杂,而且后期不易进行维护,为避免driling式数据通讯,可以采用redux进行数据通讯。在新版本React 16.8.6Context为我们带来新的通讯方式。

Context API组成部分

  • React.createContext函数:创建context上下文,参数是一个默认值(需要传递state数据),state可以是Object、Array或者基本类型数据。

  • Provider:由React.createContext创建返回对象的属性。在Redux vs. The React Context API中比喻成构建组件树中的电子总线比较形象。

  • Consumer:由React.createContext创建返回对象的属性。比喻接入电子总线获取数据。

Context vs redux

Contextcontext.Provider/Context.Consumerreduxprovider/connect非常相似。Context采用的是生产者消费者的模式,我们可以利用高阶函数(Hoc)模拟实现一个redux

redux是通过dispatch一个action去修改store数据;在React 16.8.6版本的React hooks提供的useredcuersuseContext为我们更方便通过Context+hooks的形式去打造一个属于自己redux

Context 简单例子

Context设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。

  • Class.contextType

挂载在class上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象。这能让你使用 this.context 来消费最近 Context 上的那个值。你可以在任何生命周期中访问到它,包括render 函数中。

  • Context.Consumer

让你在函数式组件中完成订阅 context。这需要函数作为子元素(function as a child)这种做法。这个函数接收当前的 context 值,返回一个 React节点。传递给函数的value值等同于往上组件树离这个 context最近的Provider 提供的 value 值。如果没有对应的 Providervalue 参数等同于传递给createContext()defaultValue

// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
    // 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
    // 无论多深,任何组件都能读取这个值。
    // 在这个例子中,我们将 “dark” 作为当前的值传递下去。
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  // 指定 contextType 读取当前的 theme context。
  // React 会往上找到最近的 theme Provider,然后使用它的值。
  // 在这个例子中,当前的 theme 值为 “dark”。
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}
// 也可以按照这种形式获取
function ThemedButton(){
    return (
      <ThemeContext.Counsumer>
        {theme =>(
         <Button theme={theme} />
         )
        }
       </ThemeContext.Counsumer>
      );
}

context的详细用法可以参考 Context文档

React Hooks

React HooksReact 16.8.6版本为函数式组件添加了在各生命周期中获取stateprops的通道。可让您在不编写类的情况下使用 state(状态) 和其他 React 功能。不再需要写class组件,你的所有组件都将是Function。如果想了解更多关于React hooks信息可以参考Hooks API 参考

基础钩子API

  • useState:获取组件state状态数据,第一个参数是保存的数据,第二参数是操作数据的方法,类似于setState。可用ES6的数组解构赋值来进行获取。
  • useEffect: 网络请求、订阅某个模块、DOM操作都是副作用,useEffect是专门用来处理副作用的。在class类组件中,componentDidMountcomponentDidUpdate生命周期函数是用来处理副作用的。
  • useContext:useContext可以很方便去订阅context的改变,并在合适的时候重渲染组件。例如上面的函数式组件中,通过Consumer的形式获取Context的数据,有了useContext可以改写成下面:
function ThemedButton(){
    const value = useContext(ThemeContxet);
    return (
         <Button theme={value} />
      );
}

useReducers API

如果习惯了redux通过reducer改变state或者props的形式,应该比较很好上手useReducersuseReducersuseContext是这篇文章比较重点的API

  • useReducersuseReducers可以传入三个参数,第一个是自定义reducer,第二参数是初始化默认值,第三个参数是一个函数,接受第二个参数进行计算获取默认值(可选)。
const [state,dispatch] = useReducer(reducer,initialValue)

下面是useReducers官方示例:

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'reset':
      return initialState;
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      // A reducer must always return a valid state.
      // Alternatively you can throw an error if an invalid action is dispatched.
      return state;
  }
}

function Counter({initialCount}) {
  const [state, dispatch] = useReducer(reducer, {count: initialCount});
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'reset'})}>
        Reset
      </button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
    </>
  );
}

简易版Redux

redux具有Provider组件,通过store进行传值。在这里,我们使用Context模拟实现Providerstore传值,完整代码可以参考 simple-redux

封装Provider组件

代码中的storeContextReact.createContext()函数创建的Context对象。this.props.store是模拟通过store传值操作。

import React,{Component} from 'react';
import {storeContext} from './store';
export default class Provider extends Component{
    render(){
        return (
            <storeContext.Provider value={this.props.store}>
                {this.props.children}
            </storeContext.Provider>
        )
    }
}

store数据管理

store文件,包括reducerContext的创建,initialStatereducers的定义。

import React from 'react';
export const storeContext = React.createContext();
export const initialState = {
    user:'kiwis',
    age:23
}
export const reducer = (state, action)=>{
    switch (action.type) {
      case 'CHANGENAME':
        return {user:'harhao',age:24}
      default:
        return initialState;
    }
}

App.js入口

在根组件App.js中,使用React hooksd的useReducer钩子函数,返回更改statedispatch函数。然后把store数据和dispatch传递进封装的Provider组件中。

import React,{useReducer} from 'react';
import Provider from './views/Provider';
import Child from './views/child';
import {initialState as store,reducer} from './views/store';
import './App.css';

function App() {
  const [state,dispatch] = useReducer(reducer,store);
  return (
    <div className="App">
      <Provider store={{state,dispatch}}>
        <Child/>
      </Provider>
    </div>
  );
}
export default App;

Child子组件

App.js的子组件Child中,通过useContext获取传递的数据statedispatch。在redux中通过connect高阶函数来传递数据。这里可以在useContext外包裹一层函数,更好模拟实现与connect相似的语法。

import React,{useContext} from 'react';
import {storeContext} from './store';
import DeepChild from './deepChild';
function Child() {
    const {state,dispatch}= useContext(storeContext);
    return (
        <div className="child">
            <p>姓名:{state.user}</p>
            <p>年龄:{state.age}</p>
            <button onClick={()=>dispatch({type:'CHANGENAME'})}>changeName</button>
            <p>deep child:</p>
            <DeepChild/>
        </div>

    );
}

export default Child;

DeepChild(孙组件)

Child子组件中,引入DeepChild组件。通过useContext获取顶层最近的state数据。

import React,{useContext} from 'react';
import {storeContext} from './store';
export default function DeepChild(){
    const {state} = useContext(storeContext);
    return (
        <div>
            {state.user}
        </div>
    )
}

运行效果

child子组件和DeepChild孙组件通过useContext获取顶层数据,最终运行效果如下所示:

demo

如果喜欢可以给个赞或星

git地址:https://github.com/Harhao/simple-redux

参考文章

React中文文档

[译]2019 React Redux 完全指南

[译] Redux vs. React 的 Context API

React Hooks 解析(上):基础

React Hooks 解析(下):进阶

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容