React的 狀態管理主要分三類:Local state、Context、Third Party
前面已經介紹了 Redux 及 Redux 家族,雖然 Redux Toolkit 已經改善及優化了很多 Redux 的複雜操作,但還有沒有更簡單好用的第三方狀態管理工具呢?讓我們來一探 Zustand 這個套件吧!
Zustand 官方表示,這是一個輕量、快速,基於 Flux 以及 Hook 概念出現的「狀態管理」套件。
Zustand 就是德語的 State
一隻拿在吉他輕鬆彈唱的熊熊是 Zustand 代表的吉祥物
之前無論是使用 Redux 還是 Context,都必須做一些前置作業設定。
但使用 Zustand 只需要設計好 Hook 後,就可以在元件中馬上使用。
Zustand 的狀態管理,是通過簡單定義的操作進行集中和更新。Redux 則必須創建 Reducer、Action、Dispatch 來處理狀態,Zustand 讓它變得更加容易。
npm install zustand
or
yarn add zustand
import create from 'zustand'
const useCounterStore = create((set, get) => ({
// 設定 state 初始值
count: 1,
// 自訂的 action function
add: () => set((state) => ({ count: state.count + 1 }))
}));
export default useCounterStore;
import useCounterStore from "./hooks/useCounterStore";
export default function App() {
const { count, add } = useCounterStore();
return (
<div>
<p>{count}</p>
<button onClick={add}>Add</button>
</div>
);
}
就這樣簡單的設定,就可以在元件中使用了!
https://codesandbox.io/s/zustand-counter-g0jtf1
前面幾篇文章使用各種方式來實作 TODO MVC,這篇當然也不例外的使用 Zustand 實作看看,也順便感受 Zustand 的輕量化。
這裡提供一個只有頁面設計尚未加上狀態管理的 CodeSandBox
加上 Zustand 及 Immer 的相依套件
為了讓在處理 Zustand 的狀態更好撰寫邏輯。官方也建議可以加上 Immer。
// hooks/useFilterStore.js
import create from "zustand";
import produce from "immer";
const initialState = "All";
const useFilterStore = create((set, get) => ({
filter: initialState,
setFilter: (_filter) => {
/*
// 沒有 immer produce 的寫法
set((state) => ({
filter: _filter
}));
*/
// 使有 immer produce 的寫法
set(
produce((state) => {
state.filter = _filter;
})
);
}
}));
export default useFilterStore;
import create from "zustand";
import produce from "immer";
import { todosAPI } from "../api";
const initialState = {
data: [],
isLoading: false,
error: false
};
const useTodosStore = create((set) => ({
todos: initialState,
addTodo: ({ id, text }) => {
set(
produce((state) => {
state.todos.data.push({
id,
text,
completed: false
});
})
);
},
toggleTodo: (id) => {
set(
produce((state) => {
const todo = state.todos.data.find((todo) => todo.id === id);
if (todo) {
todo.completed = !todo.completed;
}
})
);
},
deleteTodo: (id) => {
set(
produce((state) => {
state.todos.data = state.todos.data.filter((todo) => {
return todo.id !== id;
});
})
);
},
fetchTodos: async () => {
set(
produce((state) => {
state.todos.isLoading = true;
})
);
const response = await todosAPI.fetchTodos();
set(
produce((state) => {
state.todos.data = response.data.map(({ id, title, completed }) => {
return {
id,
text: title,
completed
};
});
state.todos.isLoading = false;
})
);
}
}));
export default useTodosStore;
只需要簡單的 import 定義好的 useStore Hook
從 Hook 中解構出需要的狀態值及變更狀態的 action 函式。(不需dispatch)
就可以輕鬆的操作狀態管理。
+import useTodosStore from "../hooks/useTodosStore";
+import useFilterStore from "../hooks/useFilterStore";
...
- const todos = [];
- const filter = "";
+ const { todos, fetchTodos } = useTodosStore();
+ const { filter, setFilter } = useFilterStore();
...
- <a
- className={classnames({ selected: filterTitle === filter })}
- style={{ cursor: "pointer" }}
- onClick={() => {}}
- >
- {filterTitle}
- </a>
+ <a
+ className={classnames({ selected: filterTitle === filter })}
+ style={{ cursor: "pointer" }}
+ onClick={() => {
+ setFilter(filterTitle);
+ }}
+ >
+ {filterTitle}
+ </a>
...
- <span
- style={{ zIndex: 10 }}
- onClick={() => {}
- >
- Load Online Todos
- </span>
+ <span
+ style={{ zIndex: 10 }}
+ onClick={() => {
+ fetchTodos();
+ }}
+ >
+ Load Online Todos
+ </span>
+ import useTodosStore from "../hooks/useTodosStore";
...
+ const { addTodo } = useTodosStore();
...
+ addTodo({
+ id: new Date().getTime().toString(),
+ text: inputRef.current.value
+ });
+ import useTodosStore from "../hooks/useTodosStore";
+ import useFilterStore from "../hooks/useFilterStore";
...
- const todos = [];
- const isLoading = false;
- const filter = "";
+ const { todos, toggleTodo, deleteTodo } = useTodosStore();
+ const { isLoading } = todos;
+ const { filter } = useFilterStore();
...
<TodoItem
key={todo.id}
todo={todo}
onToggleItem={() => {
+ toggleTodo(todo.id);
}}
onDeleteItem={() => {
+ deleteTodo(todo.id);
}}
/>
https://codesandbox.io/s/react-todomvc-zustand-4evu37
把 persist 函式包在 store function 外面,指定好 storate name 及 要使用 storage type (不指定就用 localStorage),就可以實現持久化的功能。
import { persist } from 'zustand/middleware';
...
let store = (set) => ({
fruits: ["apple", "banana", "orange"],
addFruits: (fruit) => {
set((state) => ({
fruits: [...state.fruits, fruit],
}));
},
});
// persist the created state
store = persist(store, {name: "basket"});
// create the store
const useStore = create(store);
export default useStore;
let store = (set, get) => ({
todos: initialState,
addTodo: ({ id, text }) => {...},
toggleTodo: (id) => {...},
deleteTodo: (id) => {...},
fetchTodos: async () => {...},
});
// persist the created state
store = persist(store, {
// unique name
name: "todos",
// (optional) by default, 'localStorage' is used
getStorage: () => sessionStorage
});
const useTodosStore = create(store);
https://codesandbox.io/s/react-todomvc-zustand-persist-10pplv
接下來將會介紹 React 的 CSS 解決方案,透過分析比較各種方案,開發者可以依照自己的專案適用性選擇最適配的組合。
https://github.com/pmndrs/zustand
https://vocus.cc/article/631789c8fd89780001bb0725
https://blog.yyisyou.tw/1059000a/
https://jasonlam-swatow.github.io/posts/react-state-patterns/
https://juejin.cn/post/7026232873233416223
https://juejin.cn/post/7134633741774749710