目前狀態狀態管理的套件五花八門,尤其眾多是基於Redux架構而產出的套件的套件產物,然而隨著hook的流行,也開始出現了更易於使用的套件。以下讓我們來zustand這個神奇的套件吧。

關於zustand

官方稱這個套件是可擴張式的狀態管理解決方案,讓你透過hook的方式直覺的採用狀態管理。
有趣的是,他們在想要取代Redux的企圖心,相較其他套件是比較強烈的。
並且他們也整理出他們認為過往在Redux遇到了哪些問題想要設法改善

其中我覺得閒有餘力的話,可以參考Redux的這篇看看:Stale Props and “Zombie Children”
在去年FB前端社團有人分享這篇文章Stale props and zombie children in Redux
我覺得很適合讀,會從原始碼開始解析問題點

如何使用

在以前我們無論是使用Redux還是Context,都必須要做一些前置設定
不過zustand僅需要設計好hook後,即可馬上享用:

  1. 設計state hook
1
2
3
4
5
6
7
8
import create from "zustand";

const useStore = create((set) => ({
count: 1,
add: () => set((state) => ({ count: state.count + 1 }))
}));

export default useStore;
  1. 設計Component
1
2
3
4
5
6
7
8
9
10
11
import useStore from "./hooks/useState";

export default function App() {
const { count, add } = useStore();
return (
<div class="counter">
<p>{count}</p>
<button onClick={add}>Add</button>
</div>
);
}

這樣就可以使用了,沒錯,就這樣
以下我提供一個示範的範例,來試試看吧:

zustand能超越Redux和Context嗎

zustand的企圖心蠻明顯的,他們擺明就是要提出比Redux和Context更好的解決方案

讓我們來看看他的描述:

Why zustand over redux?

  1. Simple and un-opinionated
  2. Makes hooks the primary means of consuming state
  3. Doesn’t wrap your app in context providers
  4. Can inform components transiently (without causing render)

過往的經驗中,當我要教剛入手React的人如何進行狀態管理時
很常會遇到門檻較高的問題,因為他們可能沒有經歷過狀態管理的技術轉型過程
所以很難去理解這些功能為什麼會存在會這樣去設計,這些都是可以被理解的
不過zustand簡化了很多令新人匪夷所思的設計,不用wrap Provider, hook的用法……

Why zustand over context?

  1. Less boilerplate
  2. Renders components only on changes
  3. Centralized, action-based state management

用過Context的都知道事實上它並非狀態管理的解決方案
透過Dependency Injection的方式call to action取得資料,會額外產生不必要的re-render
並且管理起來也比較分散,不集中
zustand也正解決了這些問題

使用方式

我覺得官方文件寫的蠻清晰易懂的,不過這邊也可以提幾個出來

All state

1
const state = useStore();

atomic state - 嚴格比較 strict

預設嚴格比較(old === new)

1
2
const nuts = useStore(state => state.nuts)
const honey = useStore(state => state.honey)

atomic state - 淺比較 shallow

shallow比較

1
2
3
4
5
6
7
8
9
10
import shallow from 'zustand/shallow'

// Object pick, re-renders the component when either state.nuts or state.honey change
const { nuts, honey } = useStore(state => ({ nuts: state.nuts, honey: state.honey }), shallow)

// Array pick, re-renders the component when either state.nuts or state.honey change
const [nuts, honey] = useStore(state => [state.nuts, state.honey], shallow)

// Mapped picks, re-renders the component when state.treats changes in order, count or keys
const treats = useStore(state => Object.keys(state.treats), shallow)

atomic state - 控制渲染 control rerender

可以設計compare function來避免因為狀態導致的re-render

1
2
3
4
const treats = useStore(
state => state.treats,
(oldTreats, newTreats) => compare(oldTreats, newTreats)
)

Memorized Selector

推薦避免re-render的方式是搭配使用useCallback
function

1
const fruit = useStore(useCallback(state => state.fruits[id], [id]))

Async

非同步直接寫

1
2
3
4
5
6
7
const useStore = create(set => ({
fishies: {},
fetch: async pond => {
const response = await fetch(pond)
set({ fishies: await response.json() })
}
}))

set與get

透過set與get,隨時更新取用資料

1
2
3
4
5
6
7
const useStore = create((set, get) => ({
sound: "grunt",
action: () => {
const sound = get().sound
// ...
}
})

模擬redux

如果不習慣也能改成redux的寫法,真是有趣的設計是吧?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const types = { increase: "INCREASE", decrease: "DECREASE" }

const reducer = (state, { type, by = 1 }) => {
switch (type) {
case types.increase: return { grumpiness: state.grumpiness + by }
case types.decrease: return { grumpiness: state.grumpiness - by }
}
}

const useStore = create(set => ({
grumpiness: 0,
dispatch: args => set(state => reducer(state, args)),
}))

const dispatch = useStore(state => state.dispatch)
dispatch({ type: types.increase, by: 2 })

關於Context

The store created with create doesn’t require context providers. In some cases, you may want to use contexts for dependency injection. Because the store is a hook, passing it as a normal context value may violate rules of hooks. To avoid misusage, a special createContext is provided.

在某些情境可能需要用到context,因此也提供類似的寫法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import create from 'zustand'
import createContext from 'zustand/context'

const { Provider, useStore } = createContext()

const createStore = () => create(...)

const App = () => (
<Provider createStore={createStore}>
...
</Provider>
)

const Component = () => {
const state = useStore()
const slice = useStore(selector)
...
}

小結

我覺得這是一個蠻有潛力的套件,更新頻率也蠻高的
儘管還沒打算用到,是很值得觀察的一個套件