ویرگول
ورودثبت نام
فرشید کریمی
فرشید کریمیفرشید کریمی‌ام. یه زمانی مهندسی برق خوندم، کلی توی دنیای embedded دور زدم و با سخت‌افزارها ور رفتم. چند سالیه توسعه وب رو شروع کردم و خیلی به عمیق شدن و فهم مفاهیم پیچیده علاقه‌مندم.
فرشید کریمی
فرشید کریمی
خواندن ۷ دقیقه·۳ روز پیش

استفاده از zustand برای بهبود مدیریت state ها

وقتی صحبت از مدیریت stateها در اپلیکیشنهای react ای میشه نمیشه یه نسخه برای همه اپلیکیشنها پیچید، بعضی وقت ها stick شدن به همون امکانات react مثل Context API جوابه و بعضی وقتها نیاز به کتابخونه هایی مجزا برای مدیریت stateها داریم.

درباره اینکه استراتژی مناسب برای هر موضوع چیه، امیدوارم بتونیم توی یه پست مجزا راجع بهش صحبت کنیم. به عنوان یه rule of thumb باید بگم که این دو نکته می تونه به شما بگه شاید وقتش باشه که به یک کتابخونه مدیریت state مجزا فکر کنید.

  • تعداد زیاد state ها در Context API

  • نرخ بالای بروزرسانی state ها در کامپوننت

البته core react دائما در حال توسعه است و مواردی مثل react compiler می تونه تا حدودی شرایط رو تغییر بده.

حالا بریم سر داستان خودمون. zustand قطعا توی دسته کتابخونه های مجزا از core react برای مدیریت stateها قرار می گیره. کار با zustand خیلی راحت و مشابه react عه، اول از همه بگذارید راجع به فلسفه کارکردی zustand یکم صحبت کنیم و اینکه چجوری داره کار میکنه.

فلسفه عملکردی zustand

وقتی شما با zustand یک store در جایی می سازی در حقیقت در run-time جاوااسکریپت یه محدوده ای میسازی که داخلش چیزهایی که میخوای رو ذخیره میکنی.

اول بگم lifecycle این store در وهله اول کاملا به run-time مرورگر شما بستگی داره یعنی اگر صفحه مرورگر رو reload کنید قاعدتا این store پاک میشه مگر اینکه جای دیگه ای هم persist بشه که موضوع بحث ما فعلا نیست. همونطور که توی شکل بالام مشخصه اپلیکیشن شما هر حرکتی بزنه دخلی(!) به این store به طور معمول نداره چون space یا فضای حافظه و run-time شون جداست.

حالا از این ببعدش جالب میشه، شما وقتی zustand رو به طور مثال در برنامه react تون استفاده میکنید یه wrapper ای از جنس stateهای react دورش ایجاد میشه که فرآیندهای آپدیت و rendering در react بدین وسیله handle میشه.

شکل زیر تقریبی از چیزی است که اتفاق میافته. به عبارتی برای اینکه react برای این که بتونه فرآیندهای rendering رو برعهده بگیره نیاز به چیزی داره که براش معنا و مفهوم داشته باشه که همون stateهاست. حالا ما هربار که useStore رو استفاده کنیم(بعدا خواهیم گفت)، در حقیقت کنترل re-render و render رو برعهده میگیریم.

اولین گام zustand - ساخت store

در کتابخونه zustand برای ساختن state کافیه متد create رو صدا بزنید:

import { create } from "zustand";

حالا store رو populate میکنیم:

export const useTasksStore = create<TasksState>((set) => ({ tasks, setTasks: (arg: Task[] | ((tasks: Task[]) => Task[])) => { set((state) => { return { tasks: typeof arg === "function" ? arg(state.tasks) : arg, }; }); }, currentView: "list", setCurrentView: (newView: TasksView) => set({ currentView: newView }), currentFilter: "", setCurrentFilter: (newFilter: string) => set({ currentFilter: newFilter }), }));

همونطور که میبینید تابع create یه تابع به عنوان آرگومان خودش میگیره. این تابع یک خودش یه آرگومان به نام set داره که میتونیم ازش برای مقداردهی موارد داخل store مون استفاده کنیم. (آرگومان get هم داره که بعدا میگیم به چه دردی میخوره)

طبعا ما می تونیم state ها رو به صورت پیچیده تری آپدیت کنیم و دستمون بازه اینجا. به این سبک partial update هم میگن.

setCurrentView: (newView: TasksView) => set({ currentView: newView, tasks: [] }),

همچنین تابع بالا یه آرگومان دوم هم می گیره که باهاش می تونیم کل store رو replace کنیم، مثلا در مثال زیر store مون رو می تونیم با clear ریست کنیم. البته در استفاده ازش با دقت عمل کنید چون می تونه کل store تون رو wipe کنه.

const initialState = { user: null, loggedIn: false, }; const useStore = create((set) => ({ ...initialState, clear: () => set(initialState, true), }));

خب حالا بریم این custom hook ای که ساختیم رو استفاده کنیم. منظورم همین store هست که با useStore ایجاد شده است.

برای این کار روش های متنوعی وجود داره و خیلی راحت میتونید انتخاب اشتباهی رو صورت بدید و اوضاع رو خرابش کنید. ما در ادامه خیلی گاماس گاماس با هم پیش میریم تا معایب و مزایای هر روش رو به خوبی خودتون متوجه بشید.

استفاده از zustand در کامپوننت

استفاده از الگوی atmoic تا ابد!

استفاده از zustand بدین شکل که state ها رو در متغیرهای مجزا استخراج کنیم(atomic pattern)، همیشه جوابه و توصیه خود zustand هم در غالب مواقع همین روشه.

const currentView = useTasksStore(state => state.currentView); const tasks = useTasksStore(state => state.tasks); const currentFilter = useTasksStore(state => state.currentFilter);

همونطور که واضحه در این حالت و هربار باید explictly بگیم چی ها رو می خواهیم. یعنی باید از selector استفاده کنیم، مشابه اونچه در غالب state manager ها مثل redux هم داشتیم.

اینجوری دقیقا میگیم چیارو می خواهیم و فقط state مربوط به همونا میاد توی کامپوننت مون و هربار که فقط این state عوض بشه کامپوننت ما rerender میشه. یعنی میشه با تقریبی اینو معادل این دونست برای درک بهتر وگرنه در واقعیت کمی پیچیده تره

const [currentFilter] = useState({"Current State's Value in the Store"}); . .

خب دیگه حالا هرچی توی store عوض بشه تا وقتی currentFilter عوض نشه این state موجبات rerender کامپوننت مون رو فراهم نمی کنه و این درسته!

خواندن store به صورت گرتره ای

می تونیم به روش زیر state مون رو از store بخونیم که خب اتفاقا کار هم می کنه.

const { currentFilter } = useTasksStore();

در این خط، کتابخونه zustand به طور پیشفرض در جواب useTasksStore کل store رو برمی گردونه که در واقع اگر به فلسفه zustand برگردیم یعنی stateهای کل store در این کامپوننت قرار داده میشن و این یعنی اگر هرکدوم از اون state ها آپدیت بشن این کامپوننت rerender خواهد شد. دقت کنید در این مورد object destructring هم با این که انجام شده ولی تاثیری نداره.

این اتفاق شبیه همون چیزی که توی Context API خود react رخ میده، فلذا اینگونه استفاده با هدف zustand در غالب مواقع همخوانی نداره.

خب تا همینجا کافیه که بتونید توی کامپوننتهاتون از zustand استفاده کنید و کنترل rendering رو راحتتر داشته باشید. از اینجا ببعد ما به این میپردازیم که چگونه از zustand به صورت هوشمندانهتری استفاده کنیم و اینم یادتون نره که هرجا شک کردید که کدوم راه درستتره just stick to atomic pattern که همیشه جوابه!

استفاده از zustand طبیعتا هیچ محدودیتی در عدم استفاده از کتابخونههای دیگه مدیریت state برای شما ایجاد نمی کنه و می تونید توی یه پروژه redux ای یا با context API به کارش بگیرید، اگر لازم بود.

در پایان این بخش اگر بخواهیم به صورت مرحله به مرحله آن چه در selectorهای zustand اتفاق میفته رو بگیم، این میشه:

با هر selector در حقیقت zustand اون کامپوننت رو در store خبردار یا subscriber قرار میده

که این یعنی با هر set در store:

  1. کتابخانه zustand میاد selector رو اجرا میکنه

  2. مقدار قبلی حاصل از برآورد این selector با مقدار حاصل از بخش یک مقایسه میشه

  3. اگر تغییری رخ داده بود، کامپوننت re-render میشه

  4. اگرم رخ نداده بود که هیچی به هیچی!

استفاده هوشمندانه تر از zustand

اول از همه با یه مثال react ای بگم چه مواقع الگوی atomic کد مارو یه جوری میکنه!

import { create } from "zustand"; type State = { count: number; user: { name: string; email: string }; increment: () => void; }; export const useTestStore = create<State>((set) => ({ count: 0, views: 1, user: { name: "Marco", email: "marco@example.com" }, increment: () => set((state) => ({ count: state.count + 1 })), }));

حالا استفاده ازش

import { useTestStore } from './store' export default function Example1() { const user = useTestStore(state => state.user) const count = useTestStore(state => state.count) const increment = useTestStore(state => state.increment) return ( <div> <p>Count: {count}</p> <button ={increment}>User: {user.name}</button> </div> ) }

همانطور که می بینید اگر تعداد بیشتری state داشته باشیم یه جورایی کلی باید چیز تکراری و boilerplate بنویسیم. آیا راه بهتری هم هست؟ بله!

import { useTestStore } from './store' export default function Example1() { const { user, count, increment } = useTestStore((state) => ({ user: state.user, count: state.count, increment: state.increment, })); return ( <div> <p>Count: {count}</p> <button ={increment}>User: {user.name}</button> </div> ) }

گیرش کجاست؟ فرض کنید در یک کامپوننت دیگه داشته باشیم.

import { useTestStore } from './store' export default function Example2() { const {user, views} = useTestStore(state => ({views:state.views, user:state.user})) return ( <div> <p>{user}</p> <p>User views: {views}</p> </div> ) }

حالا اگر به طور مثال از increment در Example1 استفاده کنیم یعنی روی button کلیک کنیم، این خط (سلکتور) یکبار دیگر اجرا خواهد شد تا از تغییرات احتمالی مطلع شود. (بالاتر گفته بودیم که هر selector یک subscriber است)

const { user, views } = useTestStore((state) => ({ views: state.views, user: state.user, }));

طبق تعریف increment، فقط count در store عوض میشود. سپس این خط یکبار دیگر ارزیابی شده و آبجکت دیگری دقیقا با همین مقادیر تولید خواهد کرد(چون چیزی عوض نشده است). حالا چون دو آبجکت رفرنس یکسانی ندارد، در نتیجه این کامپوننت بیخودی رندر مجدد می شود.

برای حل این مشکل باید یه جوری به zustand بگیم که آقا ما نمی خواهیم دو تا آبجکت رو با هم از نظر رفرنس مقایسه کنی و فقط همین که مقادیرش عوض بشه برامون مهمه. برای این کار از مفهوم shallow استفاده می کنیم.


مشاهده مقاله کامل در این آدرس:

https://farshidev.ir/Post/%E2%80%8Czustand-react-smart-state-managment

موفق باشید.

مدیریت statereact
۱
۰
فرشید کریمی
فرشید کریمی
فرشید کریمی‌ام. یه زمانی مهندسی برق خوندم، کلی توی دنیای embedded دور زدم و با سخت‌افزارها ور رفتم. چند سالیه توسعه وب رو شروع کردم و خیلی به عمیق شدن و فهم مفاهیم پیچیده علاقه‌مندم.
شاید از این پست‌ها خوشتان بیاید