#React: การจัดการ State ด้วย Zustand

เมื่อ React Application มีขนาดใหญ่ขึ้น การส่งข้อมูลด้วย props ผ่าน Component หลายระดับอาจทำให้โค้ดซับซ้อนขึ้น โดยเฉพาะข้อมูลที่หลายส่วนของระบบต้องใช้งานร่วมกัน เช่น ผู้ใช้ที่ Login อยู่, Shopping Cart, Theme, Filter หรือสถานะของ UI

Zustand เป็น State Management Library สำหรับ React ที่เน้น API ขนาดเล็ก ใช้งานง่าย และสามารถให้ Component เลือก Subscribe เฉพาะ State ที่ต้องการได้

หมายเหตุ: ชื่อไลบรารีสะกดว่า Zustand

#1. State ใน React มีแบบไหนบ้าง?

ก่อนเลือก Zustand ควรแยกประเภท State ให้ชัดเจน

#Local State

State ที่ใช้เฉพาะ Component เช่น

const [open, setOpen] = useState(false)

ถ้า State ไม่จำเป็นต้องแชร์ออกไป การใช้ useState ยังเป็นตัวเลือกที่ตรงที่สุด

#Shared / Global Client State

ข้อมูลที่หลาย Component ต้องใช้งาน เช่น

  • Authentication state
  • Shopping cart
  • Theme
  • Sidebar state
  • User preferences
  • Filters

State กลุ่มนี้เหมาะกับ Zustand

#Server State

ข้อมูลที่มาจาก API มีลักษณะต่างจาก Client State เพราะอาจต้องจัดการเรื่อง

  • Cache
  • Loading
  • Error
  • Refetch
  • Stale data
  • Synchronization

ในระบบขนาดใหญ่ควรพิจารณาแยก Server State ออกจาก Client State แทนการเก็บข้อมูลทุกอย่างลง Zustand


#2. ติดตั้ง Zustand

สร้าง React + TypeScript project ตัวอย่างด้วย Vite

npm create vite@latest zustand-demo -- --template react-ts
cd zustand-demo
npm install

ติดตั้ง Zustand

npm install zustand

รันระบบ

npm run dev

#3. สร้าง Store แรก

สร้างไฟล์

src/
└── stores/
    └── useCounterStore.ts
import { create } from 'zustand'

type CounterStore = {
  count: number
  increment: () => void
  decrement: () => void
  reset: () => void
}

export const useCounterStore = create<CounterStore>((set) => ({
  count: 0,

  increment: () =>
    set((state) => ({
      count: state.count + 1,
    })),

  decrement: () =>
    set((state) => ({
      count: state.count - 1,
    })),

  reset: () => set({ count: 0 }),
}))

Store ประกอบด้วยสองส่วนสำคัญ คือ State และ Actions

Zustand Store
├── State
│   └── count
└── Actions
    ├── increment()
    ├── decrement()
    └── reset()

set() ใช้สำหรับเปลี่ยน State ภายใน Store


#4. ใช้งาน Store ใน React Component

import { useCounterStore } from './stores/useCounterStore'

export default function Counter() {
  const count = useCounterStore((state) => state.count)
  const increment = useCounterStore((state) => state.increment)
  const decrement = useCounterStore((state) => state.decrement)
  const reset = useCounterStore((state) => state.reset)

  return (
    <div>
      <h1>Count: {count}</h1>

      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>Reset</button>
    </div>
  )
}

Component ไม่จำเป็นต้องรับ count ผ่าน Props จาก Parent

Component
    │
    │ selector
    ▼
Zustand Store
    │
    ├── count
    ├── increment()
    ├── decrement()
    └── reset()

#5. Selector สำคัญอย่างไร?

ตัวอย่าง

const count = useCounterStore((state) => state.count)

Component เลือก Subscribe เฉพาะ count

แนวทางนี้ควรใช้แทนการดึง Store ทั้งหมดโดยไม่จำเป็น เพราะช่วยจำกัดขอบเขตการ Subscribe และทำให้พฤติกรรมการ Render ชัดเจนขึ้น


#6. Store สำหรับ Authentication

สร้างไฟล์

src/stores/useAuthStore.ts
import { create } from 'zustand'

type User = {
  id: number
  name: string
  email: string
}

type AuthStore = {
  user: User | null
  token: string | null
  login: (user: User, token: string) => void
  logout: () => void
}

export const useAuthStore = create<AuthStore>((set) => ({
  user: null,
  token: null,

  login: (user, token) =>
    set({
      user,
      token,
    }),

  logout: () =>
    set({
      user: null,
      token: null,
    }),
}))

ใช้งาน

const user = useAuthStore((state) => state.user)
const logout = useAuthStore((state) => state.logout)

#7. Persist State ลง Local Storage

หาก Refresh หน้าเว็บ State ปกติในหน่วยความจำจะเริ่มใหม่ สามารถใช้ persist middleware เพื่อบันทึก State บางส่วนลง Storage

import { create } from 'zustand'
import { persist } from 'zustand/middleware'

type AuthStore = {
  token: string | null
  setToken: (token: string) => void
  logout: () => void
}

export const useAuthStore = create<AuthStore>()(
  persist(
    (set) => ({
      token: null,

      setToken: (token) => set({ token }),

      logout: () => set({ token: null }),
    }),
    {
      name: 'auth-storage',
    }
  )
)

ตรวจสอบได้ใน Browser DevTools → Application → Local Storage

ในระบบจริง ควรออกแบบการจัดเก็บ credential/token ตาม threat model ของระบบ ไม่ควรสรุปว่า Local Storage เหมาะกับ token ทุกประเภทโดยอัตโนมัติ


#8. เลือก State ที่ต้องการ Persist

ไม่จำเป็นต้อง Persist ทุก State

persist(
  (set) => ({
    token: null,
    sidebarOpen: false,

    setToken: (token: string) => set({ token }),
  }),
  {
    name: 'app-storage',

    partialize: (state) => ({
      token: state.token,
    }),
  }
)

sidebarOpen จะไม่ถูกบันทึก


#9. Async Action และ API

Zustand สามารถสร้าง Async Action ได้โดยตรง

import { create } from 'zustand'

type User = {
  id: number
  name: string
  email: string
}

type UserStore = {
  users: User[]
  loading: boolean
  error: string | null
  fetchUsers: () => Promise<void>
}

export const useUserStore = create<UserStore>((set) => ({
  users: [],
  loading: false,
  error: null,

  fetchUsers: async () => {
    set({
      loading: true,
      error: null,
    })

    try {
      const response = await fetch(
        'https://jsonplaceholder.typicode.com/users'
      )

      if (!response.ok) {
        throw new Error('Cannot load users')
      }

      const users: User[] = await response.json()

      set({
        users,
        loading: false,
      })
    } catch (error) {
      set({
        loading: false,
        error:
          error instanceof Error
            ? error.message
            : 'Unknown error',
      })
    }
  },
}))

ใช้งาน

import { useEffect } from 'react'
import { useUserStore } from './stores/useUserStore'

export default function Users() {
  const users = useUserStore((state) => state.users)
  const loading = useUserStore((state) => state.loading)
  const error = useUserStore((state) => state.error)
  const fetchUsers = useUserStore((state) => state.fetchUsers)

  useEffect(() => {
    fetchUsers()
  }, [fetchUsers])

  if (loading) return <p>Loading...</p>
  if (error) return <p>{error}</p>

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}

สำหรับ API ที่ซับซ้อนและต้องการ cache/refetch/invalidation อย่างเป็นระบบ อาจใช้ Server-State Library แยกต่างหาก แล้วใช้ Zustand ดูแล Client State


#10. Shopping Cart Example

import { create } from 'zustand'

type Product = {
  id: number
  name: string
  price: number
}

type CartStore = {
  products: Product[]
  addProduct: (product: Product) => void
  removeProduct: (id: number) => void
  clearCart: () => void
}

export const useCartStore = create<CartStore>((set) => ({
  products: [],

  addProduct: (product) =>
    set((state) => ({
      products: [...state.products, product],
    })),

  removeProduct: (id) =>
    set((state) => ({
      products: state.products.filter(
        (product) => product.id !== id
      ),
    })),

  clearCart: () => set({ products: [] }),
}))

คำนวณข้อมูลจาก State ผ่าน Selector ได้ เช่น

const total = useCartStore((state) =>
  state.products.reduce(
    (sum, product) => sum + product.price,
    0
  )
)

#11. อ่าน State ปัจจุบันด้วย get()

ใน Store สามารถใช้ get เพื่ออ่าน State ปัจจุบันได้

export const useCounterStore = create<CounterStore>(
  (set, get) => ({
    count: 0,

    increment: () =>
      set({
        count: get().count + 1,
      }),

    decrement: () =>
      set({
        count: get().count - 1,
      }),

    reset: () =>
      set({
        count: 0,
      }),
  })
)

โดยทั่วไป callback form ของ set เหมาะมากเมื่อ State ใหม่ขึ้นกับ State เดิม

set((state) => ({
  count: state.count + 1,
}))

#12. Redux DevTools

Zustand มี devtools middleware

import { create } from 'zustand'
import { devtools } from 'zustand/middleware'

export const useCounterStore = create<CounterStore>()(
  devtools(
    (set) => ({
      count: 0,

      increment: () =>
        set((state) => ({
          count: state.count + 1,
        })),

      decrement: () =>
        set((state) => ({
          count: state.count - 1,
        })),

      reset: () => set({ count: 0 }),
    }),
    {
      name: 'CounterStore',
    }
  )
)

ช่วยให้ตรวจสอบ State transition ระหว่างพัฒนาได้สะดวกขึ้น


#13. Persist + DevTools

สามารถประกอบ Middleware ได้

import { create } from 'zustand'
import {
  devtools,
  persist,
} from 'zustand/middleware'

export const useCounterStore = create<CounterStore>()(
  devtools(
    persist(
      (set) => ({
        count: 0,

        increment: () =>
          set((state) => ({
            count: state.count + 1,
          })),

        decrement: () =>
          set((state) => ({
            count: state.count - 1,
          })),

        reset: () => set({ count: 0 }),
      }),
      {
        name: 'counter-storage',
      }
    ),
    {
      name: 'CounterStore',
    }
  )
)

#14. โครงสร้าง Project ที่แนะนำ

สำหรับโปรเจกต์ขนาดเล็กถึงกลาง

src/
├── components/
├── pages/
├── services/
│   └── api.ts
├── stores/
│   ├── useAuthStore.ts
│   ├── useCartStore.ts
│   └── useUiStore.ts
├── types/
├── App.tsx
└── main.tsx

ระบบขนาดใหญ่สามารถจัดตาม Feature

src/
├── features/
│   ├── auth/
│   │   ├── components/
│   │   ├── store/
│   │   └── types/
│   └── cart/
│       ├── components/
│       ├── store/
│       └── types/
└── shared/

#15. Best Practices

#ใช้ Local State เมื่อข้อมูลเป็น Local จริง ๆ

ไม่จำเป็นต้องนำ State ทุกตัวเข้า Zustand

const [modalOpen, setModalOpen] = useState(false)

ถ้าใช้เฉพาะ Component เดียว นี่อาจเป็นการออกแบบที่ง่ายกว่า

#ใช้ Selector ให้แคบ

const user = useAuthStore((state) => state.user)

แทนการ Subscribe ข้อมูลจำนวนมากโดยไม่จำเป็น

#แยก Store ตาม Domain

ตัวอย่าง

useAuthStore
useCartStore
useUiStore

ทำให้ responsibility ของ Store ชัดเจน

#อย่าใช้ Zustand แทนทุกอย่าง

แนวทางหนึ่งที่เหมาะกับหลายระบบคือ

React useState
     │
     ├── Local UI State
     │
Zustand
     │
     ├── Shared Client State
     │
Server-State Tool
     │
     └── Remote/API State

#16. Data Flow

┌───────────────────────┐
│    React Component    │
└──────────┬────────────┘
           │
           │ action()
           ▼
┌───────────────────────┐
│     Zustand Store     │
│                       │
│  State                │
│  Actions              │
└──────────┬────────────┘
           │
           │ Selector
           ▼
┌───────────────────────┐
│ Subscribed Component  │
└───────────────────────┘

Optional Middleware
        │
        ├── Persist → Storage
        └── DevTools → Debugging

#17. Zustand เหมาะกับงานแบบไหน?

Zustand เหมาะเมื่อ React Application ต้องการ Shared Client State แต่ไม่ต้องการ State Management API ที่มี ceremony มาก

ตัวอย่างที่พบบ่อย:

  • Authentication UI state
  • Shopping cart
  • Theme
  • Sidebar
  • Modal state
  • Search/filter state
  • Multi-step form
  • User preferences
  • Cross-component state

แต่ถ้า State เป็นข้อมูล Remote API จำนวนมาก ควรวิเคราะห์ Server-State requirements ก่อน


#สรุป

Zustand ทำให้การสร้าง Shared State ใน React กระชับ โดยแนวคิดหลักคือ

create()
   │
   ▼
 Store
 ├── State
 └── Actions
   │
   ▼
Selector
   │
   ▼
React Component

เริ่มต้นใช้งานได้จาก

import { create } from 'zustand'

export const useStore = create((set) => ({
  count: 0,

  increment: () =>
    set((state) => ({
      count: state.count + 1,
    })),
}))

เมื่อระบบโตขึ้นจึงค่อยเพิ่ม TypeScript types, Persist, DevTools, domain-based stores และวางขอบเขตระหว่าง Client State กับ Server State ให้เหมาะสม

#เอกสารอ้างอิง