ข้ามไปยังเนื้อหา

บล็อก

การจัดการสถานะขั้นสูงใน React: การเลือกเครื่องมือที่เหมาะสมสำหรับงาน

คู่มือเชิงปฏิบัติสำหรับ Redux Toolkit, Zustand, Jotai, TanStack Query และบริบท React พร้อมรูปแบบสถาปัตยกรรมในโลกแห่งความเป็นจริงและเทคนิคด้านประสิทธิภาพ

  • อ่าน 7 นาที
  • Updated
ในหน้านี้

การจัดการของรัฐถือเป็นหัวใจสำคัญของทุกแอปพลิเคชัน React ที่ไม่สำคัญ ทำให้ถูกต้องและโค้ดเบสของคุณจะปรับขนาดได้อย่างสวยงาม หากทำผิดและคุณจะพบกับฝันร้ายที่เจาะทะลุอุปกรณ์ UI ที่เก่า และส่วนประกอบต่างๆ ที่ถูกลืมเลือนอีกครั้ง ข่าวดีก็คือว่าระบบนิเวศ React ได้เติบโตอย่างรวดเร็ว ตอนนี้คุณมีเมนูเครื่องมือที่สร้างขึ้นตามวัตถุประสงค์มากมาย เช่น Redux Toolkit, Zustand, Jotai, TanStack Query และ Context API ในตัว ซึ่งแต่ละเมนูได้รับการปรับให้เหมาะกับระดับปัญหาเฉพาะ

บทความนี้จะอธิบายวิธีแก้ปัญหาแต่ละอย่างในเชิงลึก อธิบายข้อดีข้อเสียด้วยตัวอย่างที่เป็นรูปธรรม และให้กรอบการตัดสินใจที่คุณสามารถนำไปใช้กับโครงการของคุณได้ทันที

ทำความเข้าใจกับรัฐประเภทต่างๆ

ภูมิทัศน์การจัดการสถานะ Reactต้นไม้ส่วนประกอบแอพหน้า กหน้า ขคอมพ์คอมพ์คอมพ์คอมพ์useState อาศัยอยู่ที่นี่ท้องถิ่นสำหรับแต่ละองค์ประกอบuseState / useReducerขอบเขต: ส่วนประกอบเดียวเมนูแบบเลื่อนลง อินพุต ฟิลด์แบบฟอร์ม การสลับบริบท Reactขอบเขต: ทรีย่อย / ทั่วทั้งแอป (ความถี่ต่ำ)ธีม, สถานที่, ผู้ใช้ที่ตรวจสอบสิทธิ์, แฟล็กคุณลักษณะชุดเครื่องมือ Reduxขอบเขต: ร้านค้าระดับโลกพร้อม DevToolsสถานะที่ซับซ้อน มิดเดิลแวร์ การเดินทางข้ามเวลาซูสแตนด์น้ำหนักเบาร้านค้าระดับโลกโจไทสถานะอะตอมเนื้อละเอียดแบบสอบถาม TanStack — สถานะเซิร์ฟเวอร์การดึงข้อมูล การแคช การซิงโครไนซ์ การรีเฟรชพื้นหลัง การอัพเดตในแง่ดีการตอบสนอง API, รายการแบ่งหน้า, ข้อมูลแบบเรียลไทม์ท้องถิ่นขอบเขตของรัฐทั่วโลกใช้สถานะบริบทซูสแตนด์โจไทรีดักซ์

ก่อนที่จะเข้าถึงห้องสมุดใดๆ คุณควรระบุให้ชัดเจนว่าคุณกำลังจัดการสถานะประเภทใด การรวมหมวดหมู่สถานะที่แตกต่างกันเป็นสาเหตุหลักของแอปพลิเคชัน React ที่ได้รับการออกแบบมากเกินไป

  • สถานะ UI ท้องถิ่น — ไม่ว่าดรอปดาวน์เปิดอยู่ แท็บใดที่ใช้งานอยู่ ค่าปัจจุบันของอินพุตที่ควบคุม สถานะนี้เป็นของคอมโพเนนต์เดียวหรือทรีย่อยขนาดเล็ก
  • สถานะไคลเอ็นต์ที่ใช้ร่วมกัน — ข้อมูลที่องค์ประกอบหลายรายการที่อาจอยู่ห่างไกลจำเป็นต้องอ่านหรือเขียน: ผู้ใช้ที่ได้รับการตรวจสอบสิทธิ์ในปัจจุบัน การตั้งค่าธีม ตะกร้าสินค้า
  • สถานะเซิร์ฟเวอร์ — ข้อมูลที่เริ่มต้นบนเซิร์ฟเวอร์ มีลักษณะไม่พร้อมกัน และจำเป็นต้องมีการตรวจสอบแคช การดึงข้อมูลพื้นหลังอีกครั้ง และการจัดการวงจรการโหลด/ข้อผิดพลาด
  • สถานะ URL — ตัวกรอง การแบ่งหน้า และคำค้นหาที่ควรรีเฟรชหน้าและแชร์ผ่านลิงก์ได้
  • สถานะแบบฟอร์ม — อินพุตที่ควบคุม ข้อผิดพลาดในการตรวจสอบ และสถานะการส่ง มักจะได้รับการจัดการที่ดีที่สุดโดยไลบรารีเฉพาะ เช่น React Hook Form

สิ่งเดียวที่มีผลกระทบมากที่สุดที่คุณสามารถทำได้คือต่อต้านความอยากที่จะเทรัฐทุกประเภทมาไว้ในร้านค้าระดับโลกแห่งเดียว แต่ละหมวดหมู่มีข้อกำหนดด้านความสอดคล้องที่แตกต่างกัน อายุการใช้งานที่แตกต่างกัน และความถี่ในการอัปเดตที่แตกต่างกัน

บริบท React: ตัวเลือกในตัว

บริบท API มาพร้อมกับ React และไม่จำเป็นต้องมีการขึ้นต่อกันเพิ่มเติม มันทำงานได้ดีสำหรับสถานะที่เปลี่ยนแปลงไม่บ่อยนักและถูกใช้โดยคอมโพเนนต์จำนวนมาก ตัวอย่างคลาสสิก ได้แก่ ธีม สถานที่ และอ็อบเจ็กต์ผู้ใช้ที่ได้รับการตรวจสอบสิทธิ์

// AuthContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react';

interface AuthState {
  user: User | null;
  login: (credentials: Credentials) => Promise<void>;
  logout: () => void;
}

const AuthContext = createContext<AuthState | null>(null);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);

  const login = async (credentials: Credentials) => {
    const user = await authService.login(credentials);
    setUser(user);
  };

  const logout = () => setUser(null);

  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error('useAuth must be used inside AuthProvider');
  return ctx;
}

กับดักประสิทธิภาพบริบท

บริบทมีลักษณะการทำงานที่รู้จักกันดีซึ่งทำให้นักพัฒนาจำนวนมากต้องสะดุด กล่าวคือ ผู้บริโภคทุกรายจะแสดงผลซ้ำทุกครั้งที่การอ้างอิงค่าบริบทเปลี่ยนแปลง หากคุณจัดเก็บออบเจ็กต์ขนาดใหญ่และกลายพันธุ์บ่อยครั้งไว้ในบริบทเดียว คุณจะทริกเกอร์การเรนเดอร์ซ้ำโดยไม่จำเป็นทั่วทั้งแผนผังของคุณ

กลยุทธ์ในการลดผลกระทบ ได้แก่ :

  • แบ่งบริบทตามความถี่ในการอัปเดต เก็บออบเจ็กต์ผู้ใช้ไว้ในบริบทหนึ่งและการตั้งค่า UI ไว้ในบริบทอื่น
  • จดจำวัตถุค่าด้วย useMemo ดังนั้นการอ้างอิงจะเปลี่ยนแปลงเมื่อข้อมูลเปลี่ยนแปลงจริงเท่านั้น
  • ใช้ React.memo บนส่วนประกอบของผู้บริโภคเพื่อข้ามการเรนเดอร์ซ้ำเมื่ออุปกรณ์ประกอบฉากที่พวกเขาสนใจไม่มีการเปลี่ยนแปลง

ข้อสรุปที่ตรงไปตรงมาก็คือบริบทนั้นยอดเยี่ยมสำหรับสถานะทั่วโลกที่มีความถี่ต่ำ แต่ไม่ใช่โซลูชันการจัดการสถานะที่มีจุดประสงค์ทั่วไป เมื่อคุณพบว่าตัวเองกำลังเขียนบันทึกช่วยจำที่ซับซ้อนเพื่อแก้ไขพฤติกรรมการเรนเดอร์ซ้ำของ Context ก็ถึงเวลาที่จะต้องเข้าถึงห้องสมุดเฉพาะ

ชุดเครื่องมือ Redux: ตัวเลือกระดับองค์กรที่สมบูรณ์

ชุดเครื่องมือ Redux - การไหลของข้อมูลแบบทิศทางเดียวผู้สร้างแอคชั่นจัดส่งมิดเดิลแวร์thunk / saga / คนตัดไม้ลดร้านค้าแหล่งเดียวของความจริงตัวเลือกส่วนประกอบUI แสดงผลอีกครั้งจัดส่ง (การกระทำ)ผ่านประมวลผลการกระทำคืนสถานะใหม่ได้ข้อมูลมามุมมองการอัปเดตปฏิสัมพันธ์ของผู้ใช้Redux DevToolsการดีบักการเดินทางข้ามเวลาและการตรวจสอบสถานะ

Redux มีชื่อเสียงในด้านสำเร็จรูป — ชื่อเสียงที่ได้รับในยุคก่อนชุดเครื่องมือ Redux Toolkit (RTK) กำจัดพิธีการในขณะที่ยังคงรักษาสิ่งที่ทำให้ Redux มีประสิทธิภาพ: แผนผังสถานะเดียวที่คาดเดาได้และตรวจสอบได้พร้อมกระแสข้อมูลทิศทางเดียวที่เข้มงวด

เรือ RTK createSliceซึ่งสร้างผู้สร้างการกระทำและตัวลดจากคำจำกัดความเดียว และใช้ Immer ภายใต้ประทุนเพื่อให้คุณสามารถเขียนการกลายพันธุ์ที่ดูจำเป็น แต่นำไปใช้จริงอย่างไม่เปลี่ยนรูป

// features/cart/cartSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

interface CartItem {
  id: string;
  name: string;
  quantity: number;
  price: number;
}

interface CartState {
  items: CartItem[];
  coupon: string | null;
}

const initialState: CartState = { items: [], coupon: null };

export const cartSlice = createSlice({
  name: 'cart',
  initialState,
  reducers: {
    addItem(state, action: PayloadAction<CartItem>) {
      const existing = state.items.find(i => i.id === action.payload.id);
      if (existing) {
        existing.quantity += action.payload.quantity;
      } else {
        state.items.push(action.payload);
      }
    },
    removeItem(state, action: PayloadAction<string>) {
      state.items = state.items.filter(i => i.id !== action.payload);
    },
    applyCoupon(state, action: PayloadAction<string>) {
      state.coupon = action.payload;
    },
  },
});

export const { addItem, removeItem, applyCoupon } = cartSlice.actions;
export default cartSlice.reducer;

แบบสอบถาม RTK: สถานะเซิร์ฟเวอร์ภายใน Redux

RTK จัดส่งคู่หูที่เรียกว่า RTK Query ซึ่งจัดการสถานะเซิร์ฟเวอร์โดยตรงภายในร้าน Redux โดยจะสร้างตะขอ React โดยอัตโนมัติจากคำจำกัดความปลายทางและจัดการแคช การทำให้ใช้งานไม่ได้ และการอัปเดตในแง่ดี

// services/productsApi.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

export const productsApi = createApi({
  reducerPath: 'productsApi',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  tagTypes: ['Product'],
  endpoints: (builder) => ({
    getProducts: builder.query<Product[], void>({
      query: () => '/products',
      providesTags: ['Product'],
    }),
    updateProduct: builder.mutation<Product, Partial<Product>>({
      query: ({ id, ...patch }) => ({
        url: `/products/${id}`,
        method: 'PATCH',
        body: patch,
      }),
      invalidatesTags: ['Product'],
    }),
  }),
});

export const { useGetProductsQuery, useUpdateProductMutation } = productsApi;

Redux Toolkit เป็นตัวเลือกที่เหมาะสมเมื่อคุณต้องการการดีบักการเดินทางข้ามเวลาผ่าน Redux DevTools เมื่อหลายทีมจำเป็นต้องสนับสนุนโมเดลสถานะที่ใช้ร่วมกันพร้อมแบบแผนที่บังคับใช้ หรือเมื่อคุณมีตรรกะทางธุรกิจแบบ cross-slice ที่ซับซ้อนซึ่งได้รับประโยชน์จากมิดเดิลแวร์ เช่น redux-saga หรือ redux-observable.

Zustand: รัฐระดับโลกรุ่นไลท์เวทโดยไม่ต้องมีพิธีการ

Zustand ใช้แนวทางที่แตกต่างอย่างสิ้นเชิง ไม่มีผู้ให้บริการ ไม่มีตัวลด และไม่มีประเภทการดำเนินการ คุณกำหนดร้านค้าเป็นอ็อบเจ็กต์ JavaScript ธรรมดาด้วยสถานะและวิธีการ จากนั้นใช้งานด้วยฮุกเดียว API ทั้งหมดพอดีบนหน้าจอเดียว

// stores/useCartStore.ts
import { create } from 'zustand';
import { persist, devtools } from 'zustand/middleware';

interface CartStore {
  items: CartItem[];
  addItem: (item: CartItem) => void;
  removeItem: (id: string) => void;
  totalPrice: () => number;
  clear: () => void;
}

export const useCartStore = create<CartStore>()(
  devtools(
    persist(
      (set, get) => ({
        items: [],

        addItem: (item) =>
          set((state) => {
            const existing = state.items.find((i) => i.id === item.id);
            if (existing) {
              return {
                items: state.items.map((i) =>
                  i.id === item.id
                    ? { ...i, quantity: i.quantity + item.quantity }
                    : i
                ),
              };
            }
            return { items: [...state.items, item] };
          }),

        removeItem: (id) =>
          set((state) => ({ items: state.items.filter((i) => i.id !== id) })),

        totalPrice: () =>
          get().items.reduce((sum, i) => sum + i.price * i.quantity, 0),

        clear: () => set({ items: [] }),
      }),
      { name: 'cart-storage' }
    )
  )
);

การสมัครสมาชิกและประสิทธิภาพโดยละเอียด

Zustand แก้ปัญหาการเรนเดอร์ซ้ำของ Context ผ่านการสมัครสมาชิกตามตัวเลือก ส่วนประกอบจะแสดงผลซ้ำเมื่อส่วนของสถานะที่เลือกมีการเปลี่ยนแปลงเท่านั้น

// Only re-renders when items.length changes, not on price changes
const itemCount = useCartStore((state) => state.items.length);

// Only re-renders when the total changes
const total = useCartStore((state) => state.totalPrice());

ระบบนิเวศมิดเดิลแวร์ของ Zustand ครอบคลุมถึงความคงอยู่ localStorage, การรวม DevTools, การเปลี่ยนรูปแบบ Immer และการซิงค์ URL เป็นตัวเลือกที่เหมาะสำหรับทีมที่ต้องการความสามารถระดับ Redux โดยไม่ต้องเสียค่าติดตั้งระดับ Redux

Jotai: สถานะปรมาณูได้รับแรงบันดาลใจจากการหดตัว

แบบจำลอง Jotai ระบุเป็นกราฟของอะตอม ซึ่งเป็นหน่วยสถานะขนาดเล็กที่สามารถประกอบขึ้นได้ซึ่งสามารถหาได้จากกันและกัน โมเดลนี้มีประสิทธิภาพเป็นพิเศษสำหรับสถานะไดนามิก ลองนึกถึงรายการโหนดตัวแก้ไขโดยที่แต่ละโหนดมีสถานะการเลือก/โฟกัสที่เป็นอิสระของตัวเอง หรือรูปแบบที่ซับซ้อนซึ่งการมองเห็นฟิลด์ขึ้นอยู่กับค่าฟิลด์อื่น ๆ

// atoms/filterAtoms.ts
import { atom, selector } from 'jotai';

export const searchTermAtom = atom('');
export const categoryAtom = atom<string | null>(null);
export const productsAtom = atom<Product[]>([]);

// Derived atom — recomputes only when its dependencies change
export const filteredProductsAtom = atom((get) => {
  const products = get(productsAtom);
  const term = get(searchTermAtom).toLowerCase();
  const category = get(categoryAtom);

  return products.filter((p) => {
    const matchesTerm = p.name.toLowerCase().includes(term);
    const matchesCategory = category === null || p.category === category;
    return matchesTerm && matchesCategory;
  });
});
// ProductList.tsx
import { useAtom, useAtomValue } from 'jotai';

function ProductList() {
  const [searchTerm, setSearchTerm] = useAtom(searchTermAtom);
  const filtered = useAtomValue(filteredProductsAtom);

  return (
    <>
      <input value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} />
      {filtered.map((p) => <ProductCard key={p.id} product={p} />)}
    </>
  );
}

แบบจำลองปฏิกิริยาที่ละเอียดของ Jotai หมายความว่าเมื่อคำค้นหาเปลี่ยนแปลง จะมีเฉพาะส่วนประกอบที่สมัครเป็นสมาชิกเท่านั้น filteredProductsAtom แสดงผลอีกครั้ง ส่วนประกอบที่สมัครสมาชิกเท่านั้น categoryAtom ไม่ได้ถูกแตะต้อง สิ่งนี้ทำให้ Jotai เป็นเลิศสำหรับการใช้งานกับกราฟสถานะที่หนาแน่นและพึ่งพาซึ่งกันและกัน

TanStack Query: หน้าแรกที่เหมาะสมสำหรับสถานะเซิร์ฟเวอร์

TanStack Query (เดิมชื่อ React Query) ไม่ใช่ไลบรารีการจัดการสถานะทั่วไป แต่เป็นไลบรารีสถานะเซิร์ฟเวอร์และจัดการความรับผิดชอบนั้นได้ดีกว่าสิ่งอื่นใดในระบบนิเวศ โดยทั่วไปแล้ว การดึงข้อมูล การแคช การซิงโครไนซ์ และการอัปเดตข้อมูลระยะไกลใน React โดยไม่มี TanStack Query หมายถึงการจัดการการโหลดแฟล็ก อ็อบเจ็กต์ข้อผิดพลาด และการทำให้แคชใช้ไม่ได้ด้วยตนเองในสถานะท้องถิ่นหรือร้านค้าส่วนกลาง TanStack Query ทำทุกอย่างโดยอัตโนมัติ

// hooks/useProducts.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

export function useProducts(filters: ProductFilters) {
  return useQuery({
    queryKey: ['products', filters],
    queryFn: () => api.getProducts(filters),
    staleTime: 5 * 60 * 1000,   // treat data as fresh for 5 minutes
    gcTime: 10 * 60 * 1000,     // keep unused data in cache for 10 minutes
  });
}

export function useUpdateProduct() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (update: ProductUpdate) => api.updateProduct(update),

    // Optimistic update
    onMutate: async (update) => {
      await queryClient.cancelQueries({ queryKey: ['products'] });
      const previous = queryClient.getQueryData(['products']);
      queryClient.setQueryData(['products'], (old: Product[]) =>
        old.map((p) => (p.id === update.id ? { ...p, ...update } : p))
      );
      return { previous };
    },

    onError: (_err, _update, context) => {
      queryClient.setQueryData(['products'], context?.previous);
    },

    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['products'] });
    },
  });
}

อาร์เรย์คีย์แบบสอบถามคือการแคชแบบดั้งเดิมของ TanStack Query แบบสอบถามใด ๆ ที่ใช้คีย์เดียวกันร่วมกันจะใช้รายการแคชเดียวกัน เมื่อคุณทำให้คีย์ใช้ไม่ได้ ทุกองค์ประกอบที่สมัครรับคีย์นั้นจะถูกดึงข้อมูลอีกครั้งโดยอัตโนมัติในเบื้องหลังและอัปเดตเมื่อมีข้อมูลใหม่เข้ามา

การดึงข้อมูลล่วงหน้าและไฮเดรชั่นใน Next.js

ในแอปพลิเคชัน Next.js คุณสามารถดึงข้อมูลข้อความค้นหาล่วงหน้าบนเซิร์ฟเวอร์และแยกน้ำออกจากข้อมูล HTML เพย์โหลด ซึ่งช่วยลดการโหลดสปินเนอร์สำหรับการแสดงผลหน้าแรกโดยสิ้นเชิง

// app/products/page.tsx (Next.js App Router)
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';

export default async function ProductsPage() {
  const queryClient = new QueryClient();
  await queryClient.prefetchQuery({
    queryKey: ['products', {}],
    queryFn: () => api.getProducts({}),
  });

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <ProductList />
    </HydrationBoundary>
  );
}

การรวมห้องสมุด: สถาปัตยกรรมเชิงปฏิบัติ

การใช้งานจริงมักจะได้ประโยชน์จากการรวมเครื่องมือหลายอย่างเข้าด้วยกัน โดยแต่ละเครื่องมือจะรับผิดชอบตามหมวดหมู่สถานะของตนเอง แอปพลิเคชัน React ขนาดกลางถึงใหญ่ที่มีโครงสร้างดีอาจมีลักษณะดังนี้:

  • แบบสอบถาม TanStack เป็นเจ้าของสถานะเซิร์ฟเวอร์ทั้งหมด มันดึงข้อมูลแคชและซิงโครไนซ์ข้อมูลระยะไกล ไม่มีสิ่งอื่นใดที่แตะการตอบสนอง API
  • ซูสแตนด์ เป็นเจ้าของสถานะไคลเอ็นต์ทั่วโลก เช่น ตะกร้าสินค้า เซสชันผู้ใช้ที่ได้รับการตรวจสอบสิทธิ์ การตั้งค่า UI และการประสานงานข้ามคุณลักษณะใดๆ ที่ซับซ้อนเกินไปสำหรับบริบท
  • บริบท React เป็นเจ้าของการกำหนดค่าแบบ low-churn — ธีมปัจจุบัน, สถานที่, แฟล็กคุณลักษณะ
  • useState / useReducer สถานะของคอมโพเนนต์ในเครื่องของตัวเอง — โมดอลเปิด/ปิด, ค่าฟิลด์ของฟอร์ม (หรือ React Hook Form สำหรับฟอร์มที่ซับซ้อน), การเลือกแท็บ
  • พารามิเตอร์การค้นหา URL สถานะการนำทางของตัวเอง — ตัวกรองที่ใช้งานอยู่ ลำดับการจัดเรียง หมายเลขหน้าปัจจุบัน

การแยกข้อกังวลนี้ไม่ใช่แค่ความสวยงามเท่านั้น หมายความว่าแคชสถานะเซิร์ฟเวอร์ของคุณจะไม่ถูกรบกวนด้วยสถานะ UI ร้านค้าไคลเอนต์ทั่วโลกของคุณจะไม่เต็มไปด้วยข้อมูลการตอบสนองที่ TanStack Query จะแคชได้อย่างมีประสิทธิภาพมากขึ้น และบริบทของคุณจะไม่ทริกเกอร์การเรนเดอร์ซ้ำทั้งแอปเนื่องจากมีผู้เก็บค่าที่เปลี่ยนแปลงอย่างรวดเร็วไว้ในนั้น

รูปแบบประสิทธิภาพที่ควรรู้

ตัวเลือกที่บันทึกไว้พร้อมกับการเลือกใหม่

เมื่อรับค่าที่คำนวณจากร้านค้า Redux ให้ใช้การเลือกใหม่เพื่อหลีกเลี่ยงการคำนวณใหม่ในการเรนเดอร์ทุกครั้ง RTK ส่งออกซ้ำ createSelector จากการเลือกใหม่

import { createSelector } from '@reduxjs/toolkit';

const selectItems = (state: RootState) => state.cart.items;

export const selectCartSummary = createSelector(selectItems, (items) => ({
  count: items.reduce((n, i) => n + i.quantity, 0),
  total: items.reduce((sum, i) => sum + i.price * i.quantity, 0),
}));

การแยกการเรนเดอร์ด้วย useTransition

React18's useTransition ให้คุณทำเครื่องหมายการอัปเดตสถานะว่าไม่เร่งด่วน เพื่อให้เบราว์เซอร์สามารถขัดจังหวะการอัปเดตเพื่อจัดการงานที่มีลำดับความสำคัญสูงกว่า เช่น อินพุตของผู้ใช้ สิ่งนี้มีประโยชน์อย่างยิ่งเมื่อการเปลี่ยนแปลงสถานะทำให้เกิดการเรนเดอร์ซ้ำซึ่งมีราคาแพง

const [isPending, startTransition] = useTransition();

const handleFilterChange = (value: string) => {
  startTransition(() => {
    setFilterValue(value); // expensive downstream re-render is deferrable
  });
};

หลีกเลี่ยงปัญหาเอกลักษณ์ของวัตถุ

แหล่งที่มาทั่วไปของการเรนเดอร์ Phantom อีกครั้งคือการสร้างออบเจ็กต์ใหม่หรือตัวอักษรอาร์เรย์ภายในการเรนเดอร์ useMemo and useCallback เป็นเครื่องมือของคุณที่นี่ แต่นำไปใช้เฉพาะเมื่อคุณมีหลักฐานของปัญหาด้านประสิทธิภาพเท่านั้น การจดบันทึกก่อนเวลาอันควรจะเพิ่มค่าใช้จ่ายด้านการรับรู้โดยไม่รับประกันผลประโยชน์

กรอบการตัดสินใจ

ใช้แผนผังการตัดสินใจต่อไปนี้เมื่อเลือกแนวทางการจัดการสถานะสำหรับคุณลักษณะใหม่:

  1. รัฐเป็นสถานะเฉพาะขององค์ประกอบเดียวหรือทรีย่อยขนาดเล็กหรือไม่ ใช้ useState หรือ useReducer.
  2. รัฐแสดงถึงข้อมูลที่ดึงมาจากเซิร์ฟเวอร์หรือไม่ ใช้ แบบสอบถาม TanStack.
  3. ลูกค้าทั่วโลกระบุว่ามีส่วนประกอบจำนวนมากในแผนผังต้องการหรือไม่ เข้าถึงเพื่อ ซูสแตนด์ เว้นแต่คุณจะอยู่ในโค้ดเบส Redux อยู่แล้ว
  4. สถานะเปลี่ยนแปลงน้อยครั้งและตอบสนองวัตถุประสงค์ในการกำหนดค่า (ธีม สถานที่) หรือไม่ ใช้ บริบท React.
  5. รัฐมีความเชื่อมโยงถึงกันอย่างมากกับคุณค่าที่ได้รับมามากมายหรือไม่? พิจารณา โจไท.
  6. คุณต้องการการแก้ไขข้อบกพร่องในการเดินทางข้ามเวลา โครงสร้างทางสถาปัตยกรรมที่เข้มงวด หรือเรื่องราวเกี่ยวกับอะซิงก์ที่ซับซ้อนหรือไม่? ใช้ ชุดเครื่องมือ Redux.

รูปแบบการต่อต้านทั่วไปที่ควรหลีกเลี่ยง

  • การจัดเก็บการตอบสนองของเซิร์ฟเวอร์ใน Redux หรือ Zustand ให้ TanStack Query เป็นเจ้าของข้อมูลนั้น การทำซ้ำในร้านค้าส่วนกลางจะสร้างข้อบกพร่องในการซิงโครไนซ์
  • รวบรวมทุกสิ่งไว้ในร้านค้าขนาดใหญ่เพียงแห่งเดียว สถานะที่ไม่เกี่ยวข้องซึ่งอยู่ในสไลซ์เดียวกันบังคับให้มีการมีเพศสัมพันธ์โดยไม่จำเป็น และทำให้การทดสอบยากขึ้น
  • การใช้บริบทสำหรับการอัพเดตความถี่สูง การเปลี่ยนแปลงค่าบริบททุกครั้งจะแสดงผลผู้บริโภคทั้งหมดอีกครั้ง สำหรับค่าที่เปลี่ยนแปลงในทุกการกดแป้นพิมพ์หรือเฟรมภาพเคลื่อนไหว บริบทเป็นเครื่องมือที่ไม่ถูกต้อง
  • ได้รับสถานะภายในส่วนประกอบ ค่าที่คำนวณซึ่งขึ้นอยู่กับสถานะควรเป็นตัวเลือกบันทึกหรืออะตอมที่ได้รับ ไม่ใช่การคำนวณแบบอินไลน์ที่ทำงานในทุกการเรนเดอร์
  • ละเว้นสถานะการโหลดและข้อผิดพลาด สถานะของเซิร์ฟเวอร์เป็นแบบอะซิงโครนัสโดยเนื้อแท้ พื้นผิวแบบสอบถาม TanStack isLoading, isErrorและ error สำหรับทุกคำถาม ใช้พวกเขา

บทสรุป

ภูมิทัศน์การจัดการของรัฐใน React ไม่เคยดีเท่านี้มาก่อน คุณไม่จำเป็นต้องเลือกระหว่างเฟรมเวิร์กที่หนักหน่วงกับการสร้างทุกอย่างตั้งแต่เริ่มต้นอีกต่อไป Redux Toolkit มอบความสามารถในการคาดการณ์ระดับองค์กรโดยไม่ต้องมีต้นแบบเหมือนรุ่นก่อน Zustand มอบพลังแบบเดียวกันนั้นด้วยการตั้งค่าเพียงเล็กน้อย Jotai แก้ปัญหายากๆ ด้วยกราฟสถานะไดนามิกที่มีรายละเอียดละเอียด และ TanStack Query ได้แก้ไขสถานะเซิร์ฟเวอร์อย่างแน่นอน — หากคุณยังคงจัดการการตอบสนอง API ใน useEffect and useStateคุณเป็นหนี้ตัวเองในการย้ายถิ่นฐาน

ข้อมูลเชิงลึกที่สำคัญที่สุดคือการจัดหมวดหมู่: ระบุสถานะที่คุณกำลังเผชิญอยู่ก่อน จากนั้นเลือกเครื่องมือที่ปรับให้เหมาะกับหมวดหมู่นั้น ต่อต้านสัญชาตญาณที่จะรวมทุกอย่างไว้ในระบบเดียว แนวทางแบบหลายชั้น — TanStack Query สำหรับสถานะเซิร์ฟเวอร์, Zustand สำหรับสถานะไคลเอนต์ทั่วโลก, บริบทสำหรับการกำหนดค่า และสถานะท้องถิ่นสำหรับทุกสิ่ง — สร้างฐานโค้ดที่ง่ายต่อการให้เหตุผล ทดสอบได้ง่ายขึ้น และบำรุงรักษาได้มากขึ้นเมื่อทีมและความต้องการเติบโตขึ้น