مدیریت state در ری اکت و آموزش zustand

ری اکت یک کتابخانه جاوا اسکریپت برای توسعه وب اپلیکیشنهای تعاملی است. یکی از ویژگیهای اصلی ری اکت، مدیریت استیت (State Management) است. مدیریت وضعیت به توسعهدهندگان کمک میکند تا دادههای اپلیکیشن خود را بهطور منظم بروزرسانی کنند و تغییرات را در سراسر اپلیکیشن بهروزرسانی کنند.
در برنامههای مدرن تحت وب که با کتابخانههایی مانند React توسعه داده میشوند، مدیریت وضعیت (State Management) یکی از مهمترین چالشها به شمار میرود. هر چه اندازه و پیچیدگی پروژه افزایش مییابد، اهمیت مدیریت صحیح وضعیتها بین کامپوننتها بیشتر احساس میشود.
به طور پیشفرض، React دارای سیستم مدیریت وضعیت داخلی (local state) برای هر کامپوننت است، اما این روش در پروژههای بزرگ پاسخگوی نیازها نیست؛ زیرا اشتراکگذاری دادهها میان کامپوننتهای غیرهمسطح یا پراکنده بسیار دشوار و پرهزینه میشود. اینجاست که کتابخانههایی مانند Zustand وارد عمل میشوند.
در ری اکت، هر کامپوننت میتواند یک یا چند state داشته باشد. state دادههای داخلی کامپوننت است که میتواند در طول زمان تغییر کند. برای مدیریت state یک کامپوننت، میتوان از دو روش استفاده کرد:
- استفاده از توابع
state
وsetState()
- استفاده از کتابخانههای مدیریت وضعیت
استفاده از توابع state
و setState()
برای مدیریت state یک کامپوننت با استفاده از توابع state
و setState()
، باید state کامپوننت را بهعنوان یک ویژگی تعریف کنید. بهعنوان مثال، کامپوننتی با یک state ساده بهصورت زیر تعریف میشود:
import { useState } from "react";
const MyComponent = () => {
const [state, setState] = useState(0)
return (
<div>
<h1>State: {state}</h1>
<button onClick={() => setState(state+1)}>
افزایش شمارنده
</button>
</div>
);
};
export default MyComponent
در این کد، state
یک ویژگی است که مقدار اولیه آن با استفاده از پارامتر initialState
تعیین میشود. setState()
تابعی است که مقدار state را تغییر میدهد.
اما همیشه کار به این سادگی نیست. در برخی زمان ها، عموما نیاز داریم داده ها را در یک صفحه نگه داشته و آن را بین صفحات دیگر تقسیم کنیم.
در ری اکت ساختار های props و context این کار را تا قسمتی برای من انجام میدهند. اما استفاده از کتابخانه های مخصوص این کار، میتواند امکانات بیشتری به ما ارایه کند.
آیا میدانید مدرسه پرنیان، دوره حضوری حرفه ای فرانت اند بصورت حضوری و مجازی برگزار میکند؟کلیک کنید
استفاده از کتابخانههای مدیریت state
کتابخانههای مدیریت state ، راهحلهای آمادهای برای مدیریت state ارائه میدهند. این کتابخانهها معمولاً امکانات بیشتری نسبت به استفاده مستقیم از توابع state
و setState()
ارائه میدهند.
برخی از کتابخانههای محبوب مدیریت وضعیت در ری اکت عبارتند از:
Redux
Redux یک کتابخانه مدیریت وضعیت کامل و انعطافپذیر است. Redux بر اساس مفهوم store کار میکند. store یک شیء است که state اپلیکیشن را در خود ذخیره میکند. برای تغییر مقدار state، باید یک action ایجاد کنید و آن را به store ارسال کنید.
Mobx
Mobx یک کتابخانه مدیریت وضعیت ساده و کارآمد است. Mobx بر اساس مفهوم observable کار میکند. observableها دادههایی هستند که بهطور خودکار بروزرسانی میشوند. برای تغییر مقدار state، میتوانید مقدار یک observable را تغییر دهید.
Context
Context یک API جدید در ری اکت 16.8 است. Context برای مدیریت state در سطح بالاتر استفاده میشود. برای استفاده از Context، باید یک provider و یک consumer ایجاد کنید. provider state را در خود ذخیره میکند و consumer میتواند از state استفاده کند.
کدام کتابخانه مدیریت state مناسب است؟
انتخاب کتابخانه مدیریت وضعیت مناسب به نیازهای اپلیکیشن شما بستگی دارد. اگر نیاز به یک کتابخانه مدیریت وضعیت کامل و انعطافپذیر دارید، Redux گزینه مناسبی است. اگر نیاز به یک کتابخانه مدیریت وضعیت ساده و کارآمد دارید، Mobx گزینه مناسبی است. اگر نیاز به مدیریت state در سطح بالاتر دارید، Context گزینه مناسبی است.

چرا Zustand؟
Zustand (در زبان آلمانی به معنای “وضعیت”) یک کتابخانهی سبک و مدرن برای مدیریت وضعیت در برنامههای React و Next.js است. برخلاف Redux که نیاز به boilerplate (کدهای تکراری و ساختاریافتهی زیاد) دارد، Zustand با طراحی ساده و روان، تجربهای سریعتر و راحتتر را برای توسعهدهندگان فراهم میکند.
مزایای اصلی Zustand به اختصار:
- سبک و سریع (فاقد وابستگی به context یا reducer)
- نوشتار ساده و مستقیم
- بدون نیاز به provider یا wrapper خاص
- سازگار با SSR (در Next.js)
- قابلیت استفاده از چندین store مجزا
- دارای API مشابه React Hookها
آموزش کتابخانه Zustand
Zustand یک کتابخانه مدیریت وضعیت برای ری اکت است که بر اساس مفهوم hook کار میکند. Zustand یک کتابخانه ساده و کارآمد است که برای اپلیکیشنهای کوچک و متوسط مناسب است.
Zustand یک کتابخانه مدیریت state مدرن برای React است که بر پایه Zustand Store و Zustand Hook بنا شده است.
Zustand دارای ویژگیهای زیر است:
- ساده و آسان برای استفاده: Zustand API بسیار ساده و قابل فهمی دارد.
- عملکرد بالا: Zustand برای ارائه عملکرد بالا طراحی شده است.
- قابل مقیاس: Zustand به راحتی می تواند با برنامه های بزرگ و پیچیده مقیاس بندی شود.
- قابل تست: Zustand به راحتی قابل تست است.
- جامعه فعال: Zustand دارای جامعه فعال و مفیدی است.
- موارد استفاده از Zustand:
- مدیریت state در کامپوننت های React
- مدیریت state در برنامه های React بزرگ و پیچیده
- اشتراک گذاری state بین کامپوننت های React
- ذخیره سازی state در حافظه محلی یا IndexedDB
Zustand بر اساس مفهوم hook کار میکند. hookها توابعی هستند که میتوان از آنها برای دسترسی به state و تغییر آن استفاده کرد. برای استفاده از Zustand، باید یک hook را برای state خود ایجاد کنید.
برای نصب کامند زیر:
npm install zustand
# یا
yarn add zustand
بزن بریم که شروع کنیم!
ابتدا باید یک فایل store.jsx بسازید و کد زیر را در آن قرار دهید.
import { create } from 'zustand'
const useStore = create((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
}))
export default useStore
همانطور که میبینید، در استور یک استیت تعریف کرده و مقدار اولیه آن را صفر در نظر گرفته ایم. همچنین دو فانکشن در آن تعریف کردیم که یکی، افزاینده و دیگری معادل صفر میکند.
حال در فایل دیگری مانند master.jsx کد زیر را قرار دهید:
import useStore from './store'
function BearCounter() {
const bears = useStore((state) => state.bears)
return <h1>{bears} i am zustand ♥</h1>
}
function Controls() {
const increasePopulation = useStore((state) => state.increasePopulation)
return <button onClick={increasePopulation}>one up</button>
}
function Remove() {
const removeAllBears = useStore((state) => state.removeAllBears)
return <button onClick={removeAllBears}>reset</button>
}
export default function Main(){
return(
<>
<BearCounter />
<Controls />
<Remove />
</>
)
}
همانطور که در این کد میبینید، ابتدا در هر فایلی که میخواهیم از دیتا های store استفاده کنیم، آن را فراخوانی میکنیم. سپس همان دو فانکشنی که در صفحه استور معرفی کردیم را نوشته و آنها را تعیین وضعیت میکنیم. به راحتی دیتا ها بین صفحات جابجا میشوند.
تفاوت zustand و redux
از نظر مفهومی، Zustand و Redux کاملا مشابه هستند، هر دو بر اساس یک مدل immutable state هستند. با این حال، Redux نیاز دارد که برنامه شما در context , providers شود. اما زاستند اینطور نیست.
مثال عملی کار با زاستند
بعنوان مثال اول، میخواهیم از صفحات دیگری، مانند صفحه page.jsx مقادیری را به سمت استور ارسال کرده و آن را آپدیت کنیم.
به همین منظور ابتدا با کد های زیر صفحه store را میسازیم:
import create from 'zustand';
const useUserStore = create((set) => ({
username: '',
age: 0,
updateUser: (newUsername, newAge) => set({ username: newUsername, age: newAge }),
}));
export default useUserStore;
و سپس در صفحه page.jsx مقادیری را به سمت آن ارسال کرده و آن را آپدیت میکنیم. کد زیر:
import React from 'react';
import useUserStore from './store';
const UserProfile = () => {
const { username, age, updateUser } = useUserStore();
return (
<div>
<h2>User Profile</h2>
<p>Username: {username}</p>
<p>Age: {age}</p>
<button onClick={() => updateUser('parsa ghorbanian', 32)}>Update User</button>
</div>
);
};
export default UserProfile;
به عنوان آخرین مثال، فرض بفرمایید که مقادیر دیتای ارسالی ما از یک صفحه، به Store زیاد و متنوع میباشد(مثلا محصولات یه فروشگاه). در این صورت کد زیر میتواند مقادیر ارسال شده از صفحات دیگربه استور را به استیت اصلی، push کند.
کد store:
import create from 'zustand';
const useUserStore = create((set) => ({
username: ['ali'],
updateUser: (e) => set((state) => ({ username: [...state.username, e.target.getAttribute('data-info')] })),
}));
export default useUserStore;
کد صفحه ارسال کننده فایل:
import React from 'react';
import useUserStore from './store';
const UserProfile = () => {
const { username, updateUser } = useUserStore();
return (
<div>
<h2>User Profile</h2>
<p>Username: {username[0]}</p>
<ul>
{username.map((val)=>{
return(
<li>{val}</li>
)
})}
</ul>
<button data-info='footabll' onClick={(e) => updateUser(e)}>Update User</button>
<button data-info='basketball' onClick={(e) => updateUser(e)}>Update User</button>
<button data-info='volleyball' onClick={(e) => updateUser(e)}>Update User</button>
<button data-info='golf' onClick={(e) => updateUser(e)}>Update User</button>
</div>
);
};
export default UserProfile;
و یا در مثالی دیگر استور ما میتواند بصورت حرفه ای ، اضافه حذف و آپدیت را داشته باشد. این کد استور را همیشه در نظر داشته باشید. با توضیحاتی که در بالا داده شد، این کد میتواند یک استور حرفه ای برای شما به ارمغان بیاورد.
import { create } from 'zustand'
const useCart = create((set) => ({
cart: [],
addToCart: (product) => set((state) => {
const existingProduct = state.cart.find((item) => item.id === product.id);
if (existingProduct) {
alert('exist..!');
return state;
} else {
return { cart: [...state.cart, product] };
}
}),
removeFromCart: (productId) => set((state) => ({
cart: state.cart.filter((item) => item.id !== productId)
})),
plusFromCart : (pId) => set((state)=>{
const index = state.cart.findIndex((item) => item.id === pId);
if (index !== -1) {
state.cart[index].num += 1;
return { cart: [...state.cart] };
}
})
}))
export default useCart
or///
plus: (id) => {
set((state) => {
return {
data: state.data.map(item => item.id === id ? {
...item, count:
item.count + 1
} : item)
}
})
},
minus: (id) => {
set((state) => {
return {
data: state.data.map(item => item.id === id ? {
...item, count: (item.count > 1) && item.count - 1 } : item)
}
})
},
مدیریت چند Store و ساختاردهی بهتر
در پروژههای متوسط تا بزرگ، نگهداری تمام وضعیتها در یک Store واحد میتواند باعث شلوغی و کاهش خوانایی کد شود. برای حل این مشکل، میتوانیم چندین Store جداگانه ایجاد کنیم و هر کدام را به یک حوزهی خاص اختصاص دهیم (مانند کاربر، سبد خرید، تنظیمات، و غیره).
/store
├── useCounterStore.js
├── useUserStore.js
└── useThemeStore.js
فایل useUserStore.js بسازید:
import { create } from 'zustand';
const useUserStore = create((set) => ({
user: null,
setUser: (userData) => set({ user: userData }),
logout: () => set({ user: null }),
}));
export default useUserStore;
اکنون میتوانید در هر کامپوننت، تنها Store مورد نیاز را وارد کرده و از آن استفاده کنید. این کار موجب جداسازی مسئولیتها (Separation of Concerns) و بهبود نگهداری کد در طول زمان میشود.
استفاده از Zustand در Server-Side Rendering (SSR) در Next.js
استفاده از Zustand در پروژههای Next.js که از Server-Side Rendering (SSR) بهره میبرند، نیاز به دقت بیشتری دارد؛ زیرا Zustand بهصورت پیشفرض فقط در سمت کلاینت اجرا میشود. با این حال، میتوان آن را بهگونهای پیکربندی کرد که با SSR نیز سازگار باشد.
Zustand از React Context استفاده نمیکند و به همین دلیل بهصورت ذاتی بدون مشکل در کلاینت کار میکند. اما برای prepopulate کردن وضعیت (hydrate کردن) از سمت سرور، باید از روشهایی مانند initial state یا hydrate کردن دستی استفاده کرد. بعنوان مثال فرض کنیم اطلاعات کاربر را از سرور دریافت و در Store مقداردهی اولیه میکنیم.
const useUserStore = create((set) => ({
user: null,
setUser: (user) => set({ user }),
}));
در صفحهی Next کد زیر:
export async function getServerSideProps(context) {
const res = await fetch('https://api.example.com/user');
const user = await res.json();
return {
props: {
initialUser: user,
},
};
}
و در نهایت در کامپوننت صفحه کد زیر:
import { useEffect } from 'react';
import useUserStore from '../store/useUserStore';
export default function ProfilePage({ initialUser }) {
const setUser = useUserStore((state) => state.setUser);
useEffect(() => {
setUser(initialUser);
}, [initialUser, setUser]);
const user = useUserStore((state) => state.user);
return <div>سلام، {user?.name}</div>;
}
استفاده از تابع shallow
برای مقایسه سطحی
برای جلوگیری از رندر مجدد هنگام استفاده از چند مقدار مختلف از Store، میتوان از تابع shallow
استفاده کرد مانند کد زیر:
import { shallow } from 'zustand/shallow';
const { count, name } = useUserStore(
(state) => ({ count: state.count, name: state.name }),
shallow
);
استفاده از حالت Lazy-loading یا dynamic imports
در صفحات Next.js که به Store خاصی نیاز دارند، میتوان Store را بهصورت دینامیک import کرد تا حجم initial bundle کاهش یابد مانند کد زیر:
const useCartStore = dynamic(() => import('../store/useCartStore'), { ssr: false });
ذخیرهسازی وضعیت Zustand در Local Storage
در برخی مواقع، نیاز داریم دادههای Zustand در مرورگر ذخیره شوند و پس از رفرش صفحه نیز باقی بمانند. این کار با استفاده از middleware مخصوص Zustand بهراحتی ممکن است.
Zustand این قابلیت را از طریق middleware به نام persist
فراهم میکند که اطلاعات state را در localStorage
یا sessionStorage
ذخیره میکند و هنگام بارگذاری مجدد، آنها را بازیابی مینماید.
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
const useAuthStore = create(
persist(
(set) => ({
user: null,
setUser: (user) => set({ user }),
logout: () => set({ user: null }),
}),
{
name: 'auth-storage', // کلید ذخیره در localStorage
getStorage: () => localStorage, // بهصورت پیشفرض همین است
}
)
);
export default useAuthStore;
اکنون، هر تغییری در
user
درlocalStorage
ذخیره میشود، و هنگام بارگذاری مجدد صفحه، وضعیت قبلی بازیابی خواهد شد.
در پروژههای Next.js که دارای SSR هستند، باید دقت داشته باشید که localStorage
فقط در سمت کلاینت در دسترس است.
getStorage: () => (typeof window !== 'undefined' ? localStorage : undefined)
اگر بخواهید بهصورت دستی اطلاعات را از حافظه مرورگر پاک کنید از کد زیر استفاده کنید:
useAuthStore.persist.clearStorage(); // پاکسازی localStorage مربوط به Store
امیدواریم از این مقاله مدیریت state در ری اکت و آموزش zustand نهایت استفاده را برده باشید و آن را با دوستانتان به اشتراک بگذارید. تیم تولید محتوای مدرسه اینترنتی پرنیان این مقاله را تهیه کرده است.
درباره مدیریت
شما در حال مطالعه یکی از مقالات آموزشی وبلاگ پرنیان بودید. اگر برایتان مفید بود آن را با دوستانتان به اشتراک بگذارید. من پارسا قربانیان و اینجا مدرسه فرانت اند پرنیان، میخواهیم در یک معامله برد برد، با هم به آرزوهایمان برسیم..
نوشته های بیشتر از مدیریتمطالب زیر را حتما مطالعه کنید
2 دیدگاه
به گفتگوی ما بپیوندید و دیدگاه خود را با ما در میان بگذارید.
مطالعه کردم-بسیار عاااااالی و به روز – تشکر استااااااد عزیز👌
سپاس از شما