iT邦幫忙

2022 iThome 鐵人賽

DAY 26
0
Modern Web

開始搞懂React生態系系列 第 26

Day 26 zustand - 基於 Flux 與 Hook實現狀態管理的套件

  • 分享至 

  • xImage
  •  

React 狀態管理的分類

React的 狀態管理主要分三類:Local state、Context、Third Party

前面已經介紹了 Redux 及 Redux 家族,雖然 Redux Toolkit 已經改善及優化了很多 Redux 的複雜操作,但還有沒有更簡單好用的第三方狀態管理工具呢?讓我們來一探 Zustand 這個套件吧!

Zustand

Zustand 官方表示,這是一個輕量、快速,基於 Flux 以及 Hook 概念出現的「狀態管理」套件。

Zustand 就是德語的 State

一隻拿在吉他輕鬆彈唱的熊熊是 Zustand 代表的吉祥物

如何使用

之前無論是使用 Redux 還是 Context,都必須做一些前置作業設定。

但使用 Zustand 只需要設計好 Hook 後,就可以在元件中馬上使用。

Zustand 的狀態管理,是通過簡單定義的操作進行集中和更新。Redux 則必須創建 Reducer、Action、Dispatch 來處理狀態,Zustand 讓它變得更加容易。

安裝

npm install zustand 
or 
yarn add zustand

建立 Store (useStore Hook)

  • 使用 create() 建立 useStore Hook
  • 使用 set() 改變狀態,使用 get() 取得狀態
import create from 'zustand'

const useCounterStore = create((set, get) => ({
  // 設定 state 初始值
  count: 1,
  // 自訂的 action function
  add: () => set((state) => ({ count: state.count + 1 }))
}));

export default useCounterStore;

在 Component 中使用

import useCounterStore from "./hooks/useCounterStore";

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

Demo

就這樣簡單的設定,就可以在元件中使用了!

https://codesandbox.io/s/zustand-counter-g0jtf1

使用 Zustand 實作 TODO MVC

前面幾篇文章使用各種方式來實作 TODO MVC,這篇當然也不例外的使用 Zustand 實作看看,也順便感受 Zustand 的輕量化。

這裡提供一個只有頁面設計尚未加上狀態管理的 CodeSandBox

前置作業

加上 Zustand 及 Immer 的相依套件

為了讓在處理 Zustand 的狀態更好撰寫邏輯。官方也建議可以加上 Immer

建立 Store (useStore Hook)

  • 加上 immer 的 produce 函式,就可以用很直覺的方式操作
// 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;

在 Component 中使用

只需要簡單的 import 定義好的 useStore Hook

從 Hook 中解構出需要的狀態值及變更狀態的 action 函式。(不需dispatch)

就可以輕鬆的操作狀態管理。

  • Footer (components/Footer.js)
+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>
  • Header (components/Header.js)
+ import useTodosStore from "../hooks/useTodosStore";
...
+ const { addTodo } = useTodosStore();
...
+ addTodo({
+   id: new Date().getTime().toString(),
+   text: inputRef.current.value
+ });
  • TodoList (components/TodoList.js)
+ 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);
  }}
/>

Demo

https://codesandbox.io/s/react-todomvc-zustand-4evu37

使用 Zustand 的擴充功能 - 把狀態做持久化

把 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;

Todos MVC 加上持久化

  • 這邊使用 sessionStorage 做持久化
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);   

Demo

https://codesandbox.io/s/react-todomvc-zustand-persist-10pplv

Next

接下來將會介紹 React 的 CSS 解決方案,透過分析比較各種方案,開發者可以依照自己的專案適用性選擇最適配的組合。

Reference

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


上一篇
Day 25 更有效率撰寫 Redux - Redux Toolkit
下一篇
Day 27 React 的 CSS 解決方案
系列文
開始搞懂React生態系30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言