「React进阶」只用两个自定义 Hooks 就能替代 React-Redux ?

简介: 自定义 Hooks 实践课

前言

之前有朋友问我,React Hooks 能否解决 React 项目状态管理的问题。这个问题让我思索了很久,最后得出的结论是:能,不过需要两个自定义 hooks 去实现。那么具体如何实现的呢? 那就是今天要讲的内容了。

通过本文,你能够学习以下内容:

  • useContext ,useRef ,useMemo,useEffect 的基本用法。
  • 如何将不同组件的自定义 hooks 建立通信,共享状态。
  • 合理编写自定义 hooks , 分析 hooks 之间的依赖关系。
  • 自定义 hooks 编写过程中一些细节问题。

带着如上的知识点,开启阅读之旅吧~(创作不易,希望屏幕前的你能给笔者赏个赞,以此鼓励我继续创作前端硬文。)

一 设计思路

首先,看一下要实现的两个自定义 hooks 具体功能。

  • useCreateStore 用于产生一个状态 Store ,通过 context 上下文传递 ,为了让每一个自定义 hooks useConnect 都能获取 context 里面的状态属性。
  • useConnect 使用这个自定义 hooks 的组件,可以获取改变状态的 dispatch 方法,还可以订阅 state ,被订阅的 state 发生变化,组件更新。

如何让不同组件的自定义 hooks 共享状态并实现通信呢?

首先不同组件的自定义 hooks ,可以通过 useContext 获得共有状态,而且还需要实现状态管理和组件通信,那么就需要一个状态调度中心来统一做这些事,可以称之为 ReduxHooksStore ,它具体做的事情如下:

  • 全局管理 state, state 变化,通知对应组件更新。
  • 收集使用 useConnect 组件的信息。组件销毁还要清除这些信息。
  • 维护并传递负责更新的 dispatch 方法。
  • 一些重要 api 要暴露给 context 上下文,传递给每一个 useConnect

1 useCreateStore 设计

首先 useCreateStore 是在靠近根部组件的位置的, 而且全局只需要一个,目的就是创建一个 Store ,并通过 Provider 传递下去。

使用:

const store = useCreateStore( reducer , initState )

参数:

  • reducer :全局 reducer,纯函数,传入 state 和 action ,返回新的 state 。
  • initState : 初始化 state 。

返回值:为 store 暴露的主要功能函数。

2 Store设计

Store 为上述所说的调度中心,接收全局 reducer ,内部维护状态 state ,负责通知更新 ,收集用 useConnect 的组件。

const Store = new ReduxHooksStore(reducer,initState).exportStore()

参数:接收两个参数,透传 useCreateStore 的参数。

3 useConnect设计

使用 useConnect 的组件,将获得 dispatch 函数,用于更新 state ,还可以通过第一个参数订阅 state ,被订阅的 state 改变 ,会让组件更新。

// 订阅 state 中的 number 
const mapStoreToState = (state)=>({
   
    number: state.number  })
const [ state , dispatch ] = useConnect(mapStoreToState)

参数:

  • mapStoreToState:将 Store 中 state ,映射到组件的 state 中,可以做视图渲染使用。
  • 如果没有第一个参数,那么只提供 dispatch 函数,不会订阅 state 变化带来的更新。

返回值:返回值是一个数组。

  • 数组第一项:为映射的 state 的值。
  • 数组第二项:为改变 state 的 dispatch 函数。

4 原理图

7.jpg

二 useCreateStore

export const ReduxContext = React.createContext(null)
/* 用于产生 reduxHooks 的 store */
export function useCreateStore(reducer,initState){
   
   
   const store = React.useRef(null)
   /* 如果存在——不需要重新实例化 Store */
   if(!store.current){
   
   
       store.current  = new ReduxHooksStore(reducer,initState).exportStore()
   }
   return store.current
}

useCreateStore 主要做的是:

  • 接收 reducerinitState ,通过 ReduxHooksStore 产生一个 store ,不期望把 store 全部暴露给使用者,只需要暴露核心的方法,所以调用实例下的 exportStore抽离出核心方法。

  • 使用一个 useRef 保存核心方法,传递给 Provider

三 状态管理者 —— ReduxHooksStore

接下来看一下核心状态 ReduxHooksStore 。

import {
   
    unstable_batchedUpdates } from 'react-dom'
class ReduxHooksStore {
   
   
    constructor(reducer,initState){
   
   
       this.name = '__ReduxHooksStore__'
       this.id = 0
       this.reducer = reducer
       this.state = initState
       this.mapConnects = {
   
   }
    }
    /* 需要对外传递的接口 */
    exportStore=()=>{
   
   
        return {
   
   
            dispatch:this.dispatch.bind(this),
            subscribe:this.subscribe.bind(this),
            unSubscribe:this.unSubscribe.bind(this),
            getInitState:this.getInitState.bind(this)
        }
    }
    /* 获取初始化 state */
    getInitState=(mapStoreToState)=>{
   
   
        return mapStoreToState(this.state)
    }
    /* 更新需要更新的组件 */
    publicRender=()=>{
   
   
        unstable_batchedUpdates(()=>{
   
    /* 批量更新 */
            Object.keys(this.mapConnects).forEach(name=>{
   
   
                const {
   
    update } = this.mapConnects[name]
                update(this.state)
            })
        })
    }
    /* 更新 state  */
    dispatch=(action)=>{
   
   
       this.state = this.reducer(this.state,action)
       // 批量更新
       this.publicRender()
    }
    /* 注册每个 connect  */
    subscribe=(connectCurrent)=>{
   
   
        const connectName = this.name + (++this.id)
        this.mapConnects[connectName] =  connectCurrent
        return connectName
    }
    /* 解除绑定 */
    unSubscribe=(connectName)=>{
   
   
        delete this.mapConnects[connectName]
    }
}

状态

  • reducer:这个 reducer 为全局的 reducer ,由 useCreateStore 传入。
  • state:全局保存的状态 state ,每次执行 reducer 会得到新的 state 。
  • mapConnects:里面保存每一个 useConnect 组件的更新函数。用于派发 state 改变带来的更新。

方法

负责初始化:

  • getInitState:这个方法给自定义 hooks 的 useConnect 使用,用于获取初始化的 state 。
  • exportStore:这个方法用于把 ReduxHooksStore 提供的核心方法传递给每一个 useConnect 。

负责绑定|解绑:

  • subscribe: 绑定每一个自定义 hooks useConnect 。
  • unSubscribe:解除绑定每一个 hooks 。

负责更新:

  • dispatch:这个方法提供给业务组件层,每一个使用 useConnect 的组件可以通过 dispatch 方法改变 state ,内部原理是通过调用 reducer 产生一个新的 state 。

  • publicRender:当 state 改变需要通知每一个使用 useConnect 的组件,这个方法就是通知更新,至于组件需不需要更新,那是 useConnect 内部需要处理的事情,这里还有一个细节,就是考虑到 dispatch 的触发场景可以是异步状态下,所以用 React-DOM 中 unstable_batchedUpdates 开启批量更新原则。

四 useConnect

useConnect 是整个功能的核心部分,它要做的事情是获取最新的 state ,然后通过订阅函数 mapStoreToState 得到订阅的 state ,判断订阅的 state 是否发生变化。如果发生变化渲染最新的 state 。

export function useConnect(mapStoreToState=()=>{
   
   }){
   
   
    /* 获取 Store 内部的重要函数 */
   const contextValue = React.useContext(ReduxContext)
   const {
   
    getInitState , subscribe ,unSubscribe , dispatch } = contextValue
   /* 用于传递给业务组件的 state  */
   const stateValue = React.useRef(getInitState(mapStoreToState))

   /* 渲染函数 */
   const [ , forceUpdate ] = React.useState()
   /* 产生 */
   const connectValue = React.useMemo(()=>{
   
   
       const state =  {
   
   
           /* 用于比较一次 dispatch 中,新的 state 和 之前的state 是否发生变化  */
           cacheState: stateValue.current,
           /* 更新函数 */
           update:function (newState) {
   
   
               /* 获取订阅的 state */
               const selectState = mapStoreToState(newState)
               /* 浅比较 state 是否发生变化,如果发生变化, */
               const isEqual = shallowEqual(state.cacheState,selectState)
               state.cacheState = selectState
               stateValue.current  = selectState
               if(!isEqual){
   
   
                   /* 更新 */
                   forceUpdate({
   
   })
               }
           }
       }
       return state
   },[ contextValue ]) // 将 contextValue 作为依赖项。

   React.useEffect(()=>{
   
   
       /* 组件挂载——注册 connect */
       const name =  subscribe(connectValue)
       return function (){
   
   
            /* 组件卸载 —— 解绑 connect */
           unSubscribe(name)
       }
   },[ connectValue ]) /* 将 connectValue 作为 useEffect 的依赖项 */

   return [ stateValue.current , dispatch ]
}

初始化

  • 用 useContext 获取上下文中, ReduxHooksStore 提供的核心函数。
  • 用 useRef 来保存得到的最新的 state 。
  • 用 useState 产生一个更新函数 forceUpdate ,这个函数只是更新组件。

注册|解绑流程

  • 注册: 通过 useEffect 来向 ReduxHooksStore 中注册当前 useConnect 产生的 connectValue ,connectValue 是什么马上会讲到。subscribe 用于注册,会返回当前 connectValue 的唯一标识 name 。

  • 解绑:在 useEffect 的销毁函数中,可以用调用 unSubscribe 传入 name 来解绑当前的 connectValue

connectValue是否更新组件

  • connectValue :真正地向 ReduxHooksStore 注册的状态,首先用 useMemo 来对 connectValue 做缓存,connectValue 为一个对象,里面的 cacheState 保留了上一次的 mapStoreToState 产生的 state ,还有一个负责更新的 update 函数。

  • 更新流程 : 当触发 dispatch 在 ReduxHooksStore 中,会让每一个 connectValue 的 update 都执行, update 会触发映射函数 mapStoreToState 来得到当前组件想要的 state 内容。然后通过 shallowEqual 浅比较新老 state 是否发生变化,如果发生变化,那么更新组件。完成整个流程。

  • shallowEqual : 这个浅比较就是 React 里面的浅比较,在第 11 章已经讲了其流程,这里就不讲了。

分清依赖关系

  • 首先自定义 hooks useConnect 的依赖关系是上下文 contextValue 改变,那么说明 store 发生变化,所以重新通过 useMemo 产生新的 connectValue 。所以 useMemo 依赖 contextValue。

  • connectValue 改变,那么需要解除原来的绑定关系,重新绑定。useEffect 依赖 connectValue。

局限性

整个 useConnect 有一些局限性,比如:

  • 没有考虑 mapStoreToState 可变性,无法动态传入 mapStoreToState 。
  • 浅比较,不能深层次比较引用数据类型。

五 使用与验证效果

接下来就是验证效果环节,我模拟了组件通信的场景。

根部组件注入 Store

import {
   
    ReduxContext , useConnect , useCreateStore } from './hooks/useRedux'
function  Index(){
   
   
    const [ isShow , setShow ] =  React.useState(true)
    console.log('index 渲染')
    return <div>
        <CompA />
        <CompB />
        <CompC />
        {
   
   isShow &&  <CompD />}
        <button onClick={
   
   () => setShow(!isShow)} >点击</button>
    </div>
}

function Root(){
   
   
    const store = useCreateStore(function(state,action){
   
   
        const {
   
    type , payload } =action
        if(type === 'setA' ){
   
   
            return {
   
   
                ...state,
                mesA:payload
            }
        }else if(type === 'setB'){
   
   
            return {
   
   
                ...state,
                mesB:payload
            }
        }else if(type === 'clear'){
   
    //清空
            return  {
   
    mesA:'',mesB:'' }
        }
        else{
   
   
            return state
        }
    },
    {
   
    mesA:'111',mesB:'111' })
    return <div>
        <ReduxContext.Provider value={
   
   store} >
            <Index/>
        </ReduxContext.Provider>
    </div>
}

Root根组件

  • 通过 useCreateStore 创建一个 store ,传入 reducer 和 初始化的值 { mesA:'111',mesB:'111' }
  • 用 Provider 传递 store。

Index组件

  • 有四个子组件 CompA , CompB ,CompC ,CompD 。其中 CompD 是 动态挂载的。

业务组件使用

function CompA(){
   
   
    const [ value ,setValue ] = useState('')
    const [state ,dispatch ] = useConnect((state)=> ({
   
    mesB : state.mesB }) )
    return <div className="component_box" >
        <p> 组件A</p>
        <p>组件B对我说 : {
   
   state.mesB} </p>
        <input onChange={
   
   (e)=>setValue(e.target.value)}
            placeholder="对B组件说"
        />
        <button onClick={
   
   ()=> dispatch({
   
    type:'setA' ,payload:value })} >确定</button>
    </div>
}

function CompB(){
   
   
    const [ value ,setValue ] = useState('')
    const [state ,dispatch ] = useConnect((state)=> ({
   
    mesA : state.mesA }) )
    return <div className="component_box" >
        <p> 组件B</p>
        <p>组件A对我说 : {
   
   state.mesA} </p>
        <input onChange={
   
   (e)=>setValue(e.target.value)}
            placeholder="对A组件说"
        />
        <button onClick={
   
   ()=> dispatch({
   
    type:'setB' ,payload:value })} >确定</button>
    </div>
}

function CompC(){
   
   
    const [state  ] = useConnect((state)=> ({
   
    mes1 : state.mesA,mes2 : state.mesB }) )
    return <div className="component_box" >
        <p>组件A : {
   
   state.mes1} </p>
        <p>组件B : {
   
   state.mes2} </p>
    </div>
}

function CompD(){
   
   
    const [ ,dispatch  ] = useConnect( )
    console.log('D 组件更新')
    return <div className="component_box" >
        <button onClick={
   
   ()=> dispatch({
   
    type:'clear' })} > 清空 </button>
    </div>
}
  • CompA 和 CompB 模拟组件双向通信。
  • CompC 组件接收 CompA 和 CompB 通信内容,并映射到 mes1 ,mes2 属性上。
  • CompD 没有 mapStoreToState ,没有订阅 state ,state 变化组件不会更新,只是用 dispatch 清空状态。

效果

8.gif

六 总结

本文通过两个自定义 hooks 实现了 React-Redux 的基本功能,这个模式在真实项目中可以使用吗? 我觉得如果是小型项目,是完全可以使用的,对于大型项目还是用 React Redux 或者其他成熟的状态管理工具。

相关文章
|
20天前
|
前端开发 JavaScript
React Hooks 全面解析
【10月更文挑战第11天】React Hooks 是 React 16.8 引入的新特性,允许在函数组件中使用状态和其他 React 特性,简化了状态管理和生命周期管理。本文从基础概念入手,详细介绍了 `useState` 和 `useEffect` 的用法,探讨了常见问题和易错点,并提供了代码示例。通过学习本文,你将更好地理解和使用 Hooks,提升开发效率。
53 4
|
23天前
|
前端开发
深入解析React Hooks:构建高效且可维护的前端应用
本文将带你走进React Hooks的世界,探索这一革新特性如何改变我们构建React组件的方式。通过分析Hooks的核心概念、使用方法和最佳实践,文章旨在帮助你充分利用Hooks来提高开发效率,编写更简洁、更可维护的前端代码。我们将通过实际代码示例,深入了解useState、useEffect等常用Hooks的内部工作原理,并探讨如何自定义Hooks以复用逻辑。
|
26天前
|
人工智能 自然语言处理 前端开发
SpringBoot + 通义千问 + 自定义React组件:支持EventStream数据解析的技术实践
【10月更文挑战第7天】在现代Web开发中,集成多种技术栈以实现复杂的功能需求已成为常态。本文将详细介绍如何使用SpringBoot作为后端框架,结合阿里巴巴的通义千问(一个强大的自然语言处理服务),并通过自定义React组件来支持服务器发送事件(SSE, Server-Sent Events)的EventStream数据解析。这一组合不仅能够实现高效的实时通信,还能利用AI技术提升用户体验。
137 2
|
28天前
|
前端开发 JavaScript API
探索React Hooks:前端开发的革命性工具
【10月更文挑战第5天】探索React Hooks:前端开发的革命性工具
|
8天前
|
前端开发 JavaScript 开发者
“揭秘React Hooks的神秘面纱:如何掌握这些改变游戏规则的超能力以打造无敌前端应用”
【10月更文挑战第25天】React Hooks 自 2018 年推出以来,已成为 React 功能组件的重要组成部分。本文全面解析了 React Hooks 的核心概念,包括 `useState` 和 `useEffect` 的使用方法,并提供了最佳实践,如避免过度使用 Hooks、保持 Hooks 调用顺序一致、使用 `useReducer` 管理复杂状态逻辑、自定义 Hooks 封装复用逻辑等,帮助开发者更高效地使用 Hooks,构建健壮且易于维护的 React 应用。
20 2
|
13天前
|
前端开发 开发者
React 提供的其他重要 Hooks
【10月更文挑战第20天】React 提供了一系列强大的 Hooks,除了 `useRef` 之外,还有许多其他重要的 Hooks,它们共同构成了函数式组件开发的基础。
29 6
|
20天前
|
前端开发 JavaScript 开发者
React Hooks
10月更文挑战第13天
31 1
|
27天前
|
前端开发
|
25天前
|
前端开发 JavaScript API
自定义React Hooks综合指南
本文介绍了React Hooks及其在组件开发中的作用,重点讲解了自定义Hook的创建和使用方法。通过实例展示了如何创建`useWindowWidth`、`useFetch`和`useForm`等自定义Hook,并分享了使用自定义Hook的最佳实践。文章强调了自定义Hook在提高代码复用性和组件可维护性方面的重要性。
40 0
|
26天前
|
存储 前端开发 JavaScript
深入理解React组件的生命周期与Hooks
【10月更文挑战第7天】深入理解React组件的生命周期与Hooks
66 0