你再也不用使用 Redux、Mobx、Flux 等狀態管理了

nusr發表於2019-05-08

Unstated Next README 的中文翻譯

前言

這個庫的作者希望使用 React 內建 API ,直接實現狀態管理的功能。看完這個庫的說明後,沒有想到程式碼可以這個玩。短短几行程式碼,僅僅使用 React Hooks ,就實現了狀態管理的功能。

看完之後,第一想法就是翻譯成中文,分享給其他人。提交 Pull Request 後,庫作者將我的翻譯合併了。同時作者歡迎將 README 翻譯成其他語言,以下是全部翻譯內容,不妥之處歡迎指正或 Pull Request.

Unstated Next

永遠不必再考慮 React 狀態管理了,僅僅 200 位元組的狀態管理解決方案。

  • React Hooks React Hooks 用做你所有的狀態管理。
  • ~200 bytes min+gz.
  • 熟悉的 API 僅僅使用了 React,沒有依賴第三方庫。
  • 最小 API 只需 5 分鐘學習。
  • TypeScript 編寫 推斷程式碼更容易,易於編寫 React 程式碼。

但是,最重要的問題是:這比 Redux 更好嗎? 答案可能是。

  • 它更小。 比 Redux 小 40 倍。
  • 它更快。 元件效能問題。
  • 它更容易學習。 你必須已經知道 React Hooks 和 Context 。只需使用它們,它們就會嗨起來。
  • 更容易整合。 一次整合一個元件,並且輕鬆與其他 React 庫整合。
  • 它更容易測試。 測試 reducers 純屬浪費你的時間,這個庫使你更容易測試 React 元件。
  • 它更容易進行型別檢查。 旨在使你的大多數型別可推斷。
  • 它是最小的。 僅僅使用了 React 。

你自己看著辦吧!

檢視 Unstated 遷移手冊 →

安裝

npm install --save unstated-next
複製程式碼

Example

import React, { useState } from "react"
import { createContainer } from "unstated-next"
import { render } from "react-dom"

function useCounter() {
  let [count, setCount] = useState(0)
  let decrement = () => setCount(count - 1)
  let increment = () => setCount(count + 1)
  return { count, decrement, increment }
}

let Counter = createContainer(useCounter)

function CounterDisplay() {
  let counter = Counter.useContainer()
  return (
    <div>
      <button onClick={counter.decrement}>-</button>
      <span>{counter.count}</span>
      <button onClick={counter.increment}>+</button>
    </div>
  )
}

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

render(<App />, document.getElementById("root"))
複製程式碼

API

createContainer(useHook)

import { createContainer } from "unstated-next"

function useCustomHook() {
  let [value, setInput] = useState()
  let onChange = e => setValue(e.currentTarget.value)
  return { value, onChange }
}

let Container = createContainer(useCustomHook)
// Container === { Provider, useContainer }
複製程式碼

<Container.Provider>

function ParentComponent() {
  return (
    <Container.Provider>
      <ChildComponent />
    </Container.Provider>
  )
}
複製程式碼

Container.useContainer()

function ChildComponent() {
  let input = Container.useContainer()
  return <input value={input.value} onChange={input.onChange} />
}
複製程式碼

useContainer(Container)

import { useContainer } from "unstated-next"

function ChildComponent() {
  let input = useContainer(Container)
  return <input value={input.value} onChange={input.onChange} />
}
複製程式碼

指南

如果你以前從未使用過 React Hooks,我不建議你往下看,請先閱讀 [React 官網的 React Hooks 文件](reactjs.org/docs/hooks-…

首先,使用 React Hooks,你可以建立這樣一個元件:

function CounterDisplay() {
  let [count, setCount] = useState(0)
  let decrement = () => setCount(count - 1)
  let increment = () => setCount(count + 1)
  return (
    <div>
      <button onClick={decrement}>-</button>
      <p>You clicked {count} times</p>
      <button onClick={increment}>+</button>
    </div>
  )
}
複製程式碼

然後,如果你想共享元件的邏輯,你可以把它寫在元件外面,自定義一個 hook:

function useCounter() {
  let [count, setCount] = useState(0)
  let decrement = () => setCount(count - 1)
  let increment = () => setCount(count + 1)
  return { count, decrement, increment }
}

function CounterDisplay() {
  let counter = useCounter()
  return (
    <div>
      <button onClick={counter.decrement}>-</button>
      <p>You clicked {counter.count} times</p>
      <button onClick={counter.increment}>+</button>
    </div>
  )
}
複製程式碼

但是,除了共享邏輯之外,你還想共享狀態,你會怎麼做呢?

這個時候,context 就發揮了作用:

function useCounter() {
  let [count, setCount] = useState(0)
  let decrement = () => setCount(count - 1)
  let increment = () => setCount(count + 1)
  return { count, decrement, increment }
}

let Counter = createContext(null)

function CounterDisplay() {
  let counter = useContext(Counter)
  return (
    <div>
      <button onClick={counter.decrement}>-</button>
      <p>You clicked {counter.count} times</p>
      <button onClick={counter.increment}>+</button>
    </div>
  )
}

function App() {
  let counter = useCounter()
  return (
    <Counter.Provider value={counter}>
      <CounterDisplay />
      <CounterDisplay />
    </Counter.Provider>
  )
}
複製程式碼

這很棒,也很完美,更多人應該編寫這樣的程式碼。

但有時我們需要更多的結構和特定的 API 設計才能使其始終保持正確。

通過引入 createContainer() 函式,你可以將自定義 hooks 作為 containers,並且定義明確的 API,防止錯誤使用。

import { createContainer } from "unstated-next"

function useCounter() {
  let [count, setCount] = useState(0)
  let decrement = () => setCount(count - 1)
  let increment = () => setCount(count + 1)
  return { count, decrement, increment }
}

let Counter = createContainer(useCounter)

function CounterDisplay() {
  let counter = Counter.useContainer()
  return (
    <div>
      <button onClick={counter.decrement}>-</button>
      <p>You clicked {counter.count} times</p>
      <button onClick={counter.increment}>+</button>
    </div>
  )
}

function App() {
  return (
    <Counter.Provider>
      <CounterDisplay />
      <CounterDisplay />
    </Counter.Provider>
  )
}
複製程式碼

下面是前後的程式碼對比:

- import { createContext, useContext } from "react"
+ import { createContainer } from "unstated-next"

  function useCounter() {
    ...
  }

- let Counter = createContext(null)
+ let Counter = createContainer(useCounter)

  function CounterDisplay() {
-   let counter = useContext(Counter)
+   let counter = Counter.useContainer()
    return (
      <div>
        ...
      </div>
    )
  }

  function App() {
-   let counter = useCounter()
    return (
-     <Counter.Provider value={counter}>
+     <Counter.Provider>
        <CounterDisplay />
        <CounterDisplay />
      </Counter.Provider>
    )
  }
複製程式碼

如果你正在使用 TypeScript(我鼓勵你瞭解更多關於它的資訊),這也有助於 TypeScript 的內建推斷做得更好。只要你的自定義 hook 型別是完善的,那麼型別都會自動推斷。

提示

提示 #1: 組合 Containers

因為我們只使用了自定義 React hooks,所以可以在其他 hooks 內部組合 containers。

function useCounter() {
  let [count, setCount] = useState(0)
  let decrement = () => setCount(count - 1)
  let increment = () => setCount(count + 1)
  return { count, decrement, increment, setCount }
}

let Counter = createContainer(useCounter)

function useResettableCounter() {
  let counter = Counter.useContainer()
  let reset = () => counter.setCount(0)
  return { ...counter, reset }
}
複製程式碼

提示 #2: 保持 Containers 很小

這對於保持 containers 小而集中非常有用。 如果你想在 containers 中對程式碼進行邏輯拆分,那麼這一點非常重要。只需將它們移動到自己的 hooks 中,僅儲存 containers 的狀態即可。

function useCount() {
  return useState(0)
}

let Count = createContainer(useCount)

function useCounter() {
  let [count, setCount] = Count.useContainer()
  let decrement = () => setCount(count - 1)
  let increment = () => setCount(count + 1)
  let reset = () => setCount(0)
  return { count, decrement, increment, reset }
}
複製程式碼

提示 #3: 優化元件

unstated-next 無需優化。所有你要做的優化,都是標準的 React 優化。

1) 通過拆分元件來優化耗時的子樹

優化前:

function CounterDisplay() {
  let counter = Counter.useContainer()
  return (
    <div>
      <button onClick={counter.decrement}>-</button>
      <p>You clicked {counter.count} times</p>
      <button onClick={counter.increment}>+</button>
      <div>
        <div>
          <div>
            <div>SUPER EXPENSIVE RENDERING STUFF</div>
          </div>
        </div>
      </div>
    </div>
  )
}
複製程式碼

優化後:

function ExpensiveComponent() {
  return (
    <div>
      <div>
        <div>
          <div>SUPER EXPENSIVE RENDERING STUFF</div>
        </div>
      </div>
    </div>
  )
}

function CounterDisplay() {
  let counter = Counter.useContainer()
  return (
    <div>
      <button onClick={counter.decrement}>-</button>
      <p>You clicked {counter.count} times</p>
      <button onClick={counter.increment}>+</button>
      <ExpensiveComponent />
    </div>
  )
}
複製程式碼

2) 使用 useMemo() 優化耗時的操作

優化前:

function CounterDisplay(props) {
  let counter = Counter.useContainer()

  // 每次 `counter` 改變都要重新計算這個值,非常耗時
  let expensiveValue = expensiveComputation(props.input)

  return (
    <div>
      <button onClick={counter.decrement}>-</button>
      <p>You clicked {counter.count} times</p>
      <button onClick={counter.increment}>+</button>
    </div>
  )
}
複製程式碼

優化後:

function CounterDisplay(props) {
  let counter = Counter.useContainer()

  // 僅在輸入更改時重新計算這個值
  let expensiveValue = useMemo(() => {
    return expensiveComputation(props.input)
  }, [props.input])

  return (
    <div>
      <button onClick={counter.decrement}>-</button>
      <p>You clicked {counter.count} times</p>
      <button onClick={counter.increment}>+</button>
    </div>
  )
}
複製程式碼

3) 使用 React.memo()、useCallback() 減少重新渲染次數

優化前:

function useCounter() {
  let [count, setCount] = useState(0)
  let decrement = () => setCount(count - 1)
  let increment = () => setCount(count + 1)
  return { count, decrement, increment }
}

let Counter = createContainer(useCounter)

function CounterDisplay(props) {
  let counter = Counter.useContainer()
  return (
    <div>
      <button onClick={counter.decrement}>-</button>
      <p>You clicked {counter.count} times</p>
      <button onClick={counter.increment}>+</button>
    </div>
  )
}
複製程式碼

優化後:

function useCounter() {
  let [count, setCount] = useState(0)
  let decrement = useCallback(() => setCount(count - 1), [count])
  let increment = useCallback(() => setCount(count + 1), [count])
  return { count, decrement, increment }
}

let Counter = createContainer(useCounter)

let CounterDisplayInner = React.memo(props => {
  return (
    <div>
      <button onClick={props.decrement}>-</button>
      <p>You clicked {props.count} times</p>
      <button onClick={props.increment}>+</button>
    </div>
  )
})

function CounterDisplay(props) {
  let counter = Counter.useContainer()
  return <CounterDisplayInner {...counter} />
}
複製程式碼

與 Unstated 的關係

我認為這個庫是 Unstated 精神的繼承者。因為我相信 React 在狀態管理方面已經非常出色,唯一缺少的就是輕鬆共享狀態和邏輯,所以我建立了 Unstated 。我建立的 Unstated 是 React 共享狀態和邏輯的 最小 解決方案。

然而,使用 Hooks,React 在共享狀態和邏輯方面可以做得更好。我甚至認為 Unstated 成為了不必要的抽象。

但是,我認為很多開發人員都在努力瞭解如何使用 React Hooks 共享狀態和邏輯,從而實現應用程式共享狀態。這可能只是文件和社群動力的問題,但我認為一個新的 API 可以彌補這種心理差距。

這個 API 就是 Unstated Next。 它不是 React 中共享狀態和邏輯的最小 API,而是用於理解如何在 React 中共享狀態和邏輯的最小 API

我一直給 React 站隊。我希望 React 可以贏。 我希望社群放棄像 Redux 這樣的狀態管理庫,並找到使用 React 內建工具鏈的更好方法。

如果你不想使用 Unstated,你只想使用 React 本身,我非常鼓勵你這麼做。 寫關於它的部落格文章! 討論它! 在社群中傳播你的知識。

unstated 遷移

我故意將其釋出為單獨的包,因為它是對原有 API 的完全重寫。 這樣,你可以逐步安裝和遷移。

請向我提供有關該遷移過程的反饋,因為在接下來的幾個月裡,我希望得到這些反饋並做以下兩件事:

  • 確保 unstated-next 滿足 unstated 使用者的所有需求。
  • 確保 unstated 使用者的程式碼可以完整地遷移到 unstated-next

我可以將 API 新增到兩者的任意一個倉庫中,從而使開發人員工作得更輕鬆。 對於 unstated-next,我將保證新增的 API 儘可能小,同時,我也會盡量保持庫很小。

未來,我可能會將 unstated-next 合併為 unstated 的主要版本。 unstated-next 仍然存在,這樣你就可以安裝 unstated@2unstated-next。 當你完成遷移後,你可以更新到 unstated@3 ,同時刪除 unstated-next(確保更新你所有的引入,這應該只是一個查詢和替換的過程)。

儘管這是一個重大的 API 更改,我希望你儘可能輕鬆地完成此遷移。我正在使用最新的 React Hooks API ,為你進行優化,而不是使用原有的 Unstated.Container 程式碼。請隨意提供有關如何做得更好的反饋。

首發 nusr.github.io/

相關文章