HooX: 應該不是 React 下一代狀態管理工具

前言

其實在一個多月前,我也已經在掘金髮過hoox 的介紹。不過我以爲這麼簡單的東西,也沒太大技術含量,也就是隨便發發看,圖個樂。最近發現基於 Hook 的狀態管理器愈來愈多了,那我也就再在這裏趕個集好了,省得之後再發顯得有點兒山寨。javascript

另外,我仍是提早說,目前個人這個小玩具,仍是 0.x 的版本。我還不敢發正式版,一方面是我本身以爲還有些未完善之處。另外一方面,是它確實沒有通過很是多項目的考驗。不過,若是純按業務流量來講,它已經在螞蟻保險幾個百萬~千萬 UV 級的 C 端頁面上跑了好久了。目前來看,沒有明顯異常。這也是我發文章時稍微有的一丁點兒底氣。vue

迴歸正文:java

爲何又要造輪子

hook 自帶輪子光環

關於 react hook 我就很少介紹了。hook 提供了抽象狀態的能力,天然而然讓人想到能夠基於 hook 抽離全局狀態。其天生自帶輪子光環,因此社區也出現了很多基於 hook 的狀態管理工具,好比說前陣子飛冰團隊出的icestore,亦或者這個stamen,不過相對來講我更喜歡的仍是這個unstated-nextreact

那既然別人都已經造了那麼多輪子了,爲何本身還要造呢?天然是由於:git

別人的輪子不夠用

好比說unstated-next,它本質上是把一個自定義 hook 全局化了。理念很好,就是狀態邏輯比較複雜的話,寫起來有點兒累。必須把 state、actions、effects 維持在一個自定義 hook 中。內部的一系列 actions、effects 須要加 useCallback、useMemo 也比較麻煩,若是抽離到外部,又要傳不少參數。總之,若是項目相對比較複雜,寫起來比較累。github

stamen  其實也不錯。聲明一個 store,包含 state、reducer、effects。並且不須要給組件包裹 Provider,各個地方隨意拔插,響應更新。就是 dispatch 我不太喜歡用,不太好直接定位到 action 或 effect 的聲明,且丟了入參出參類型。redux

icestore  的問題也差很少。說是支持 TS,實際上是殘缺的,看了下源碼,類型徹底都丟失了。另外命名空間這一套我也不是很喜歡。api

另外,前兩天螞蟻體驗技術部的同窗也出了一個hox。名字跟個人這個很像,但確實不是一個東西。它呢有點兒像 unstated-next 跟 statemen 的結合體。按我理解,它核心就是想在 unstated-next 的基礎上,解決嵌套Provider的問題。不過這也不是我使用 unstated-next 時的痛點。另外,其內部使用ReactDOM.render來實現,無法實現 SSR。數組

固然上述這些問題人家也能優化。可是何須呢,原本也沒幾行代碼,給人家提 PR 的時間,我本身都寫好輪子了。因此總而言之,仍是本身造吧。async

個人理想型

那我本身想要的狀態管理工具是怎麼樣的呢?在 hoox 以前呢,其實我還實現了一版,基本複製 dva 的 api 的一個版本(把 yield 換成 async/await )。有點兒像 icestore,只不過沒有命名空間。但它有着 icestore 跟 stamen 一樣的問題,不太好直接定位到 action/effect 的聲明。

後來我總結了一下,我真正想要的是怎麼樣的:

  1. 全局狀態管理,但非單一 store;
  2. actions 跟 effects 就是正常的函數,獨立聲明,直接引用;
  3. 完美的 TS 支持。

因此目標很簡單,能夠說就是  unstated-next  的去 hook 包裹版。因而我實現了一版,最終效果以下:

HooX

建立全局狀態

// store.js
import createHoox from 'hooxjs'

// 聲明全局初始狀態
const state = {
  count: 1
}

// 建立store
export const {
  Provider, // 使用全局狀態的組件或者其根組件,須要被Provider包裹
  useHoox, // 獲取全局狀態,以及更新全局狀態的方法,相似useState
  getHoox // 獲取全局狀態,相比useHoox,其獲取的狀態更新時,並不會觸發組件更新,經常使用於effect跟action中
} = createHoox(state)

// 建立一個 action
export const up = () => {
  const [{ count }, setHoox] = getHoox()
  return setHoox({ count: count + 1 })
}

// 建立一個 effect
export const effectUp = async () => {
  // getHoox 跟 useHoox
  const [{ count }, setHoox] = getHoox()
  const newState = { count: count + 1 }
  await fetch('/api/up', newState)
  return setHoox(newState)
  // 或者直接引用action
  // return up()
}

固然,若是action/effect場景簡單的話,也有些簡單的 api。

export const {
  // ...其餘api
  setHoox
} = createHoox(state)

// 建立一個 action
export const up = () => setHoox(({ count }) => ({ count: count + 1 }))

能夠看到,經過這樣的方式,建立action/effect以及全局狀態就脫離 hook 了。這樣的好處有:

  1. action/effect不在 hook 中,避免每次 render 致使的函數從新聲明(進而須要useCallback/useMemo)。
  2. 可方便的將方法抽離到其餘文件,下降單個文件複雜度。

消費狀態

在組件裏使用全局狀態,爲了保證響應式,須要經過useHoox獲取。若是是使用action/effect,那就比較簡單了,直接引用便可。

切忌,組件不該該經過getHoox獲取全局狀態,由於它不具備響應式的邏輯。雖然也能獲取到狀態,可是並不會由於狀態的變動而觸發組件 render。

import { useHoox, up, effectUp } from './store'

function Counter() {
  const [state] = useHoox()
  return (
    <div>
      <div>{state.count}</div>
      <button onClick={up}>up</button>
      <button onClick={effectUp}>effectUp</button>
    </div>
  )
}

直接修改狀態

若是場景較爲簡單,且不須要抽象action,也能夠直接在組件內部更新狀態。

import { useHoox } from './store'

function Counter() {
  const [state, setHoox] = useHoox()
  return (
    <div>
      <div>{state.count}</div>
      <input
        value={state.count}
        onChange={event => setHoox({ count: event.target.value })}
      />
    </div>
  )
}

若是這個組件只更改狀態,不須要消費狀態,也能夠直接用setHoox

import { setHoox } from './store'

function Inputer() {
  return (
    <div>
      <input onChange={event => setHoox({ count: event.target.value })} />
    </div>
  )
}

重置狀態

咱們知道,在 class 組件中,經過 this.setState  是作狀態的合併更新。可是在 function 組件中, useState  返回的第二個參數 setState  又是作替換更新。實際使用中,其實咱們都有訴求。尤爲是非 TS 的項目,狀態模型多是動態的,極可能須要作重置狀態。爲了知足全部人的需求,我也加了個 api 方便你們使用

import { useHoox } from './store';

function Counter() {
  const [state, setHoox, resetHoox] = useHoox()
  return (
    <div>
      {state ? <div>{state.count}</div> : null}
      <button onClick={() => resetHoox(null)>reset</button>
    </div>
  )
}

全局 computed

經過上述 api,其實咱們還能夠實現相似 vue 中 computed  的效果。

import { useHoox } from './store'

export function useDoubleCount() {
  const [{ count }] = useHoox()
  return count * 2
}

對於某些很是複雜的運算,咱們也可使用 react 的 useMemo  作優化。

import { useHoox } from './store'

export function usePowCount(number = 2) {
  const [{ count }] = useHoox()
  return useMemo(() => Math.pow(count, number), [count, number])
}

除此外,也能夠實現一些全局 hooks。

connect

其實正常來講,個人業務代碼基本不太會寫connect的...直接useHoox便可。但也有兩種狀況是例外的:

  1. 引用了某些通用性的組件,想經過 connect props 來解耦全局狀態邏輯。
  2. 一個是之前就寫好的class組件不想改形成function組件,但又要用到全局狀態。

因此實現了connect這個 api,方便解決這兩個問題。

首先 store 中須要暴露出這個 api。

// store.js
export const {
  // ...其餘api
  connect
} = createHoox(state)

而後對於函數式組件:

// Counter.js
import { connect } from './store'

const Counter = ({ count }) => {
  return <div>{count}</div>
}

const NewCounter = connect(state => ({ count: state.count }))(Counter)

export default NewCounter

connect之後,返回的NewCounter,就不須要再接受count這個prop,這個也已經作好了類型推導。

若是想用裝飾器的話,函數組件是沒有辦法的,不過class能夠。

import { connect } from './store'

@connect(state => ({ count: state.count }))
export default class Counter extends React.PureComponent {
  render() {
    return <div>{this.props.count}</div>
  }
}

但這個裝飾器僅限於 js 環境,ts 環境下,裝飾器不能改變 class 的返回類型。可是實際代碼中,組件被connect後,我會返回一個新的函數式組件,而且改變了組件Props的類型(去除了全局狀態注入的 props)。所以 ts 環境下,沒法正常使用裝飾器。固然 使用函數包裹依舊是能夠的:

import { connect } from './store'

class Counter extends React.PureComponent<{ count: number }> {
  render() {
    return <div>{this.props.count}</div>
  }
}

export default connect(state => ({ count: state.count }))(Counter)

不夠美好的地方

須要 Provider

hoox 底層基於contextuseState實現,因爲把狀態存在context了中,故而相似 Redux,消費狀態的組件必須是相應Context.Provider的子孫組件。如:

import { Provider } from './store'
import Counter from './counter'

function App() {
  return (
    <Provider>
      <Counter />
    </Provider>
  )
}

這進而致使了,若是一個組件須要消費兩個 store,那就須要成爲兩個Provider的子孫組件。

hoox 提供了一個語法糖createContainer,能夠稍微的簡化一下語法。

import { createContainer } from './store'
import Counter from './counter'

function App() {
  return <Counter />
}

export default createContainer(App)

雖然這樣仍是有些繁瑣。尤爲當有多個 store 互相調用的時候,須要特殊注意,用到狀態的組件是否在相應的 Provider 包裹下。但我依舊不肯意使用相似stamenhox這樣發佈訂閱的方法。由於 react 已經有一套本身響應式邏輯了。再在上面加個發佈訂閱的邏輯...我能力比較差,hold 不住... 目前我也想不到更好的辦法,只能提供下語法糖,稍微簡化一點點。

幾個 api 第一次使用會分不清

getHooxuseHooxsetHoox什麼的,確實 api 看着比較多,第一次用會有點兒懵。可能還會用錯。不過新手用只要切記一點:沒什麼特殊要求,不要在組件裏使用getHoox 只要牢記這點,基本只要能跑通,就沒啥大問題。

用一小段時間後,明白getHoox是非響應式地獲取全局狀態,後續就 OK 了。最近團隊裏有個同窗再研究eslint-plugin。後續讓他幫忙寫個hoox的 lint 插件就能改善一部分問題了。

目前來看,我也找不到其餘更好的辦法能解決這個問題,我必須有這幾個api

其餘很差的地方

留給評論區

寫在最後

這個工具,目前咱們團隊內有 5-6 我的使用。總體而言,口碑還行,尤爲是對於一些中小項目。有些組件稍微繁瑣一些,就會有一堆 useMemo 來,useCallback 去的邏輯。經過hoox,將這些邏輯抽離出 hook,代碼會清爽很多。另外,這些中小項目,引入dva/redux這些工具,確實顯得偏重。經過函數式組件+hoox,即保證了輕量級,也知足了全局狀態管理的場景。

可是呢,它確實還有很多缺點。並且若是是真的想吃透 99%的場景,可能還須要補充一些配套工具。包括提到的lint插件,甚至是相似redux-devtools這樣的工具。目前的我,還不敢發正式版,也不敢拿本身部門來背書。因此這篇文章,包括在掘金,我都沒有發到部門專欄。

不過若是你想用,基本仍是能夠放心的用。咱們本身已經有多條業務在使用了,不是大版本,不可能 breaking change 了。只是說,它不必定是 React 狀態管理工具的終態......將來大家遇到更好的,仍是可能會選擇遷移。

最後總結一下就是:問題不大,歡迎使用!

Github

具體的源碼跟詳細 api 介紹能夠見 github:https://github.com/wuomzfx/hoox

關於源碼部分我就不詳細說明啦,也沒幾行代碼,看看就能明白。

相關文章
相關標籤/搜索