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
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:
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:
// 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:
// 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ư:
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.
// 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:
// 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
// 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.
// 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;
// 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:
// 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
Vercel là gì? Hướng dẫn deploy dự án Next.js bằng Vercel
Dec 07, 2024 • 14 min read
So sánh giữa HOCs, Render Props và Hooks.
Dec 05, 2024 • 8 min read
Render Props pattern là gì? Hướng dẫn sử dụng Render Props
Dec 03, 2024 • 8 min read
HOCs Pattern là gì? Hướng dẫn triển khai Hocs Pattern trong dự án React
Dec 02, 2024 • 7 min read
Hooks Pattern là gì? Hướng dẫn áp dụng Hooks Pattern trong dự án React
Nov 28, 2024 • 11 min read
Promise là gì? Hướng dẫn sử dụng Promise trong dự án React
Nov 27, 2024 • 7 min read