Facebook Pixel

Hướng dẫn sử dụng Zustand trong NextJS

21 Nov, 2024

Tran Thuy Vy

Frontend Developer

Zustand là một thư viện quản lý trạng thái nhẹ cho React, giúp bạn quản lý trạng thái dễ dàng mà không cần phải viết nhiều code phức tạp

Hướng dẫn sử dụng Zustand trong NextJS

Mục Lục

Hello, lại là mình đây, hôm nay mình muốn chia sẻ với các bạn về một thư viện quản lý state mà mình đã trải nghiệm gần đây và thấy khá là ấn tượng, đó là Zustand. Nếu bạn đã từng đau đầu với Redux vì quá nhiều cấu hình phức tạp, hoặc cảm thấy Context API của React chưa đáp ứng đủ nhu cầu, thì Zustand có thể là giải pháp bạn đang tìm kiếm.

Trong bài viết này, mình sẽ hướng dẫn cách sử dụng Zustand trong dự án NextJS. Mình sẽ cố gắng giải thích từng phần  cách dễ hiểu nhất, kèm theo các ví dụ với hy vọng các bạn sẽ nắm được.

1. Zustand là gì?

Zustand là một thư viện quản lý trạng thái nhẹ cho React, được phát triển bởi nhóm pmndrs. Nó cung cấp một API đơn giản, linh hoạt và hiệu suất cao, giúp bạn quản lý trạng thái một cách dễ dàng mà không cần phải viết nhiều code phức tạp.

2. Tại sao bạn nên chọn Zustand?

  • Kích thước nhỏ (~1KB nén gzip) và hiệu suất cao.
  • Dễ tích hợp và sử dụng trong ứng dụng, không cần nhiều cấu hình.
  • Tránh được các vấn đề render lại không cần thiết.
  • Phù hợp với nhiều loại dự án, từ nhỏ đến lớn.

Ví dụ như thay vì bạn phải viết nhiều reducer và action như trong Redux, với Zustand, bạn chỉ cần định nghĩa state và các action một cách đơn giản.

3. Hướng dẫn sử dụng Zustand trong dự án NextJS

Để tạo một dự án NextJS cơ bản, bạn có thể tham khảo thêm tại đây

Tiếp theo, bạn hãy chạy câu lệnh bên dưới để cài đặt zustand nha

Bash
pnpm install zustand

Nếu như bạn sử dụng TypeScript, cũng không cần cài đặt thêm vì Zustand đã bao gồm phần định nghĩa TypeScript rồi nhé.

  • Tạo và sử dụng Store trong Zustand

Trong Zustand, store là nơi lưu trữ trạng thái của ứng dụng. Bạn có thể tạo nhiều store tùy theo nhu cầu, và mỗi store là một hook sử dụng trong component.

Cấu trúc chung của một store trong Zustand sẽ trông như thế này:

Typescript
import create from 'zustand';

interface State {
  // Định nghĩa state
}

const useStore = create<State>((set) => ({
  // Khởi tạo state và action
}));

export default useStore;

Cụ thể hơn thì đoạn code trông như thế này:

Typescript
// stores/counterStore.ts
import create from 'zustand';

interface CounterState {
  count: number;
  increase: () => void;
  decrease: () => void;
  reset: () => void;
}

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

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

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

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

export default useCounterStore;

import create from 'zustand': dùng để tạo store.

interface CounterState: định nghĩa type cho state và các actions trong store. Lý do bạn nên định nghĩa type để dễ bảo trì, an toàn, hỗ trợ IntelliSense.

  • count: number: lưu trữ số đếm.
  • increase: () => void: hàm tăng số đếm.
  • decrease: () => void: hàm giảm số đếm.
  • reset: () => void: hàm đặt lại số đếm.

const useCounterStore = create<CounterState>((set) => ({ ... })): tạo store với type CounterState.

  • set: hàm dùng để cập nhật trạng thái
  • Khởi tạo giá trị ban đầu của count là 0.
  • increase: () => set((state) => ({ count: state.count + 1 })): cập nhật state
  • (state) => ({ count: state.count + 1 }): hàm nhận trạng thái hiện tại và trả về trạng thái mới với count tăng lên 1.

Sử dụng Store trong component:

TSX
// pages/index.tsx
import React from 'react';
import useCounterStore from '../stores/counterStore';

const HomePage: React.FC = () => {
  // get count value from store
  const count = useCounterStore((state) => state.count);

  // action from store
  const increase = useCounterStore((state) => state.increase);
  const decrease = useCounterStore((state) => state.decrease);
  const reset = useCounterStore((state) => state.reset);

  return (
    <div style={{ textAlign: 'center', marginTop: '50px' }}>
      <h1>Giá trị đếm: {count}</h1>
      <button onClick={increase}>Tăng</button>
      <button onClick={decrease}>Giảm</button>
      <button onClick={reset}>Đặt lại</button>
    </div>
  );
};

export default HomePage;
  • Sử dụng hook useCounterStore để truy cập giá trị count.
  • Selector (state) => state.count giúp lấy ra phần state cần thiết. Cá nhân mình khuyên nên sử dụng Selector nha bởi vì khi bạn sử dụng selectors giúp component của bạn chỉ render lại khi state đó thay đổi, giúp tối ưu hiệu suất.

Ví dụ như:

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

Component sẽ chỉ re-render khi state.count thay đổi, không phụ thuộc vào các phần khác của trạng thái.

Bạn có thể sử dụng cùng một store trong nhiều component khác nhau.

TSX
// components/Header.tsx
import React from 'react';
import useCounterStore from '../stores/counterStore';

const Header: React.FC = () => {
  const count = useCounterStore((state) => state.count);

  return (
    <header style={{ backgroundColor: '#f0f0f0', padding: '10px' }}>
      <h2>Giá trị đếm trong Header: {count}</h2>
    </header>
  );
};

export default Header;
  • Component Header cũng sử dụng useCounterStore để truy cập count.
  • Khi giá trị count thay đổi, cả HomePage và Header đều được cập nhật.

4. Một vài tính năng nâng cao của Zustand

4.1 Sử dụng middleware

Zustand hỗ trợ các middleware để mở rộng chức năng của store. Ví dụ như sử dụng devtools để tích hợp với Redux DevTools:

Typescript
// stores/counterStore.ts
import create from 'zustand';
import { devtools } from 'zustand/middleware';

interface CounterState {
  count: number;
  increase: () => void;
  decrease: () => void;
  reset: () => void;
}

const useCounterStore = create<CounterState>(
  devtools(
    (set) => ({
      count: 0,
      increase: () => set((state) => ({ count: state.count + 1 })),
      decrease: () => set((state) => ({ count: state.count - 1 })),
      reset: () => set({ count: 0 }),
    }),
    { name: 'CounterStore' }
  )
);

export default useCounterStore;

devtools((set) => ({ ... }), { name: 'CounterStore' }): bọc hàm tạo store với middleware devtools.

name: 'CounterStore': là tên hiển thị trong Redux DevTools.

Cài đặt Redux DevTools Extension trên browser để theo dõi trạng thái.

4.2 Lưu trữ state với persist

Bạn có thể sử dụng persist để lưu trạng thái vào localStorage

Typescript
// stores/counterStore.ts
import create from 'zustand';
import { persist } from 'zustand/middleware';

interface CounterState {
  count: number;
  increase: () => void;
  decrease: () => void;
  reset: () => void;
}

const useCounterStore = create<CounterState>(
  persist(
    (set) => ({
      count: 0,
      increase: () => set((state) => ({ count: state.count + 1 })),
      decrease: () => set((state) => ({ count: state.count - 1 })),
      reset: () => set({ count: 0 }),
    }),
    {
      name: 'counter-storage',
    }
  )
);

export default useCounterStore;
  • persist((set) => ({ ... }), { name: 'counter-storage' }): tạo store với middleware persist.
  • name: 'counter-storage': tên key trong localStorage để lưu trạng thái.
  • Trạng thái count sẽ được lưu trong localStorage, và khi bạn reload page, giá trị sẽ được khôi phục.

4.3 Kết hợp nhiều store

Hơn nữa, bạn có thể tạo nhiều store khác nhau và sử dụng trong ứng dụng.

Typescript
// stores/userStore.ts
import create from 'zustand';

interface UserState {
  user: User | null;
  setUser: (user: User) => void;
  clearUser: () => void;
}

interface User {
  id: number;
  name: string;
  email: string;
}

const useUserStore = create<UserState>((set) => ({
  user: null,
  setUser: (user) => set({ user }),
  clearUser: () => set({ user: null }),
}));

export default useUserStore;
TSX
// components/UserProfile.tsx
import React from 'react';
import useUserStore from '../stores/userStore';

const UserProfile: React.FC = () => {
  const user = useUserStore((state) => state.user);
  const setUser = useUserStore((state) => state.setUser);

  React.useEffect(() => {
    const fetchedUser = {
      id: 1,
      name: 'Trần Vy',
      email: 'ttv.thuyvy.1544@gmail.com',
    };
    setUser(fetchedUser);
  }, [setUser]);

  if (!user) {
    return <p>Đang tải thông tin người dùng...</p>;
  }

  return (
    <div>
      <h2>Thông tin người dùng</h2>
      <p>Tên: {user.name}</p>
      <p>Email: {user.email}</p>
    </div>
  );
};

export default UserProfile;
  • Sử dụng useUserStore để truy cập user và setUser.
  • Trong useEffect, mình giả lập việc lấy thông tin người dùng và cập nhật trạng thái.
  • Hiển thị thông tin người dùng sau khi có dữ liệu.

5. Sử dụng Zustand với NextJS trong Server-Side Rendering

Khi sử dụng NextJS với SSR, trạng thái trên server và client có thể bất đồng bộ, dẫn đến lỗi hydration.

Ví dụ: nếu bạn lấy dữ liệu trên server và muốn khởi tạo trạng thái với dữ liệu đó, bạn cần đảm bảo rằng trạng thái trên client khớp với server.

Sử dụng getServerSideProps hoặc getStaticProps để lấy dữ liệu trên server:

TSX
// pages/ssr.tsx
import React from 'react';
import usePostStore from '../stores/postStore';

interface SSRPageProps {
  initialPosts: Post[];
}

const SSRPage: React.FC<SSRPageProps> = ({ initialPosts }) => {
  const setPosts = usePostStore((state) => state.setPosts);

  React.useEffect(() => {
    setPosts(initialPosts);
  }, [initialPosts, setPosts]);

  const posts = usePostStore((state) => state.posts);

  return (
    <div>
      <h1>Bài viết (SSR)</h1>
      {posts.map((post) => (
        <div key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.content}</p>
        </div>
      ))}
    </div>
  );
};

export async function getServerSideProps() {
  const response = await fetch('https://example.com/api/posts');
  const data: Post[] = await response.json();

  return {
    props: {
      initialPosts: data,
    },
  };
}

export default SSRPage;
  • Trong getServerSideProps, bạn lấy dữ liệu trên server và truyền vào component qua props.
  • Trong component, sử dụng useEffect để cập nhật trạng thái store với dữ liệu ban đầu.
  • Điều này đảm bảo rằng trạng thái trên server và client đồng bộ, tránh lỗi hydration.

Bên cạnh đó, bạn cần phải đảm bảo state trên server và client khớp với nhau.

6. Kết luận

Qua bài viết này, mình đã đưa các bạn đi qua hướng dẫn về cách sử dụng Zustand trong dự án NextJS, TypeScript. Chúng ta đã tìm hiểu từ việc tạo store cơ bản, sử dụng trong component, đến các tính năng nâng cao như middleware và tích hợp với SSR.

Zustand thật sự là công cụ mạnh mẽ và linh hoạt, giúp việc quản lý state trong ứng dụng React và NextJS trở nên đơn giản hơn rất nhiều. Nếu bạn đang tìm kiếm một giải pháp nhẹ nhàng, dễ sử dụng và hiệu quả, hãy thử áp dụng Zustand vào dự án của bạn nhé.

Các bài viết liên quan:

Bài viết liên quan

Lập trình backend expressjs

xây dựng hệ thống microservices
  • Kiến trúc Hexagonal và ứng dụngal font-
  • TypeScript: OOP và nguyên lý SOLIDal font-
  • Event-Driven Architecture, Queue & PubSubal font-
  • Basic scalable System Designal font-

Đăng ký nhận thông báo

Đừng bỏ lỡ những bài viết thú vị từ 200Lab