ایجاد حالت سراسری در ری اکت با قلاب و بدون context — از صفر تا صد

۱۴۴ بازدید
آخرین به‌روزرسانی: ۰۳ مهر ۱۴۰۲
زمان مطالعه: ۴ دقیقه
ایجاد حالت سراسری در ری اکت با قلاب و بدون context — از صفر تا صد

در این مقاله با روش ساخت یک کتابخانه برای ایجاد حالت سراسری در ری‌اکت با استفاده از قلاب و بدون نیاز به context آشنا می‌شویم. امیدواریم این راهنمای گام به گام به شما کمک کند با ماهیت و روش ساخت این حالت سراسری آشنا شوید. کد کامل این کتابخانه را می‌توانید در این ریپوی گیت‌هاب (+) ملاحظه کنید.

گام 1: متغیر سراسری

در ابتدا باید یک متغیر سراسری داشته باشیم:

1let globalState = {
2  count: 0,
3  text: 'hello',
4};

در سراسر این مقاله از همین ساختار استفاده می‌کنیم.

در ادامه یک قلاب ری‌اکت برای خواندن این متغیر سراسری می‌سازیم:

1const useGlobalState = () => {
2  return globalState;
3};

البته این عملاً یک قلاب ری‌اکت نیست، زیرا به قلاب‌های ابتدایی ری‌اکت وابستگی ندارد. در واقع این چیزی نیست که معمولاً انتظار داریم، زیرا در زمان تغییر یافتن متغیر سراسری، رندر مجد نمی‌شود.

گام 2: رندر مجدد در زمان به‌روزرسانی

ما باید از قلاب useState در ری‌اکت برای واکنشی ساختن قلاب خودمان استفاده کنیم:

1const listeners = new Set();
2const useGlobalState = () => {
3  const [state, setState] = useState(globalState);
4  useEffect(() => {
5    const listener = () => {
6      setState(globalState);
7    };
8    listeners.add(listener);
9    listener(); // in case it's already changed
10    return () => listeners.delete(listener); // cleanup
11  }, []);
12  return state;
13};

بدین ترتیب امکان به‌روزرسانی حالت ری‌اکت از خارج فراهم می‌شود. اگر متغیر سراسری را به‌روزرسانی کنیم، باید به شنونده‌ها اطلاع دهیم. بنابراین در ادامه یک تابع برای به‌روزرسانی می‌سازیم:

1const setGlobalState = (nextGlobalState) => {
2  globalState = nextGlobalState;
3  listeners.forEach(listener => listener());
4};

بدین ترتیب می‌توانیم useGlobalState را طوری تغییر دهیم که یک چندتایی مانند useState بازگشت دهد:

1const useGlobalState = () => {
2  const [state, setState] = useState(globalState);
3  useEffect(() => {
4    // ...
5  }, []);
6  return [state, setGlobalState];
7};

گام 3: کانتینر

به طور معمول، متغیر سراسری در یک «دامنه فایل» (File Scope) قرار دارد. آن را در دامنه تابع قرار می‌دهیم تا کمی محدودتر شده و خوانایی آن افزایش یابد.

1const createContainer = (initialState) => {
2  let globalState = initialState;
3  const listeners = new Set();
4  const setGlobalState = (nextGlobalState) => {
5    globalState = nextGlobalState;
6    listeners.forEach(listener => listener());
7  };
8  const useGlobalState = () => {
9    const [state, setState] = useState(globalState);
10    useEffect(() => {
11      const listener = () => {
12        setState(globalState);
13      };
14      listeners.add(listener);
15      listener(); // in case it's already changed
16      return () => listeners.delete(listener); // cleanup
17    }, []);
18    return [state, setGlobalState];
19  };
20  return {
21    setGlobalState,
22    useGlobalState,
23  };
24};

در این مقاله قصد نداریم وارد جزییات کد تایپ اسکریپت شویم، اما فرم فوق به ما امکان می‌دهد که انواع useGlobalState را با استنباط از انواع initialState حاشیه‌نویسی (annotate) کنیم.

گام 4: دسترسی دامنه‌دار

با این که امکان ساخت چندین کانتینر وجود دارد، اما به طور معمول چندین آیتم را در یک حالت سراسری قرار می‌دهیم.

کتابخانه‌های معمول برای حالت سراسری کارکردهای دامنه‌ای را صرفاً برای بخشی از حالت ارائه می‌کنند. برای نمونه React-Redux از یک اینترفیس سلکتور برای به دست آوردن مقدار مشتق شده از یک حالت سراسری بهره می‌گیرد.

ما در این کتابخانه از رویکرد ساده‌تری استفاده می‌کنیم و از یک کلید رشته‌ای برای حالت سراسری بهره می‌گیریم. در این مثال از count و text استفاده کرده‌ایم.

1const createContainer = (initialState) => {
2  let globalState = initialState;
3  const listeners = Object.fromEntries(Object.keys(initialState).map(key => [key, new Set()]));
4  const setGlobalState = (key, nextValue) => {
5    globalState = { ...globalState, [key]: nextValue };
6    listeners[key].forEach(listener => listener());
7  };
8  const useGlobalState = (key) => {
9    const [state, setState] = useState(globalState[key]);
10    useEffect(() => {
11      const listener = () => {
12        setState(globalState[key]);
13      };
14      listeners[key].add(listener);
15      listener(); // in case it's already changed
16      return () => listeners[key].delete(listener); // cleanup
17    }, []);
18    return [state, (nextValue) => setGlobalState(key, nextValue)];
19  };
20  return {
21    setGlobalState,
22    useGlobalState,
23  };
24};

به منظور سادگی بیشتر در این کد از useCallback استفاده نمی‌کنیم، اما استفاده از آن در کتابخانه‌ها به طور کلی توصیه شده است.

گام 5: به‌روزرسانی‌های تابعی

با استفاده از useState در ری‌اکت امکان انجام به‌روزرسانی‌های تابعی فراهم می‌آید. این خصوصیت را نیز پیاده‌سازی می‌کنیم:

1// ...
2  const setGlobalState = (key, nextValue) => {
3    if (typeof nextValue === 'function') {
4      globalState = { ...globalState, [key]: nextValue(globalState[key]) };
5    } else {
6      globalState = { ...globalState, [key]: nextValue };
7    }
8    listeners[key].forEach(listener => listener());
9  };
10  // ...

گام 6: Reducer

افرادی که با ریداکس آشنا هستند، استفاده از اینترفیس Reducer را ترجیح می‌دهند. قلاب ری‌اکت به نام useReducer نیز اساساً همان اینترفیس را ارائه می‌کند.

1const createContainer = (reducer, initialState) => {
2  let globalState = initialState;
3  const listeners = Object.fromEntries(Object.keys(initialState).map(key => [key, new Set()]));
4  const dispatch = (action) => {
5    const prevState = globalState;
6    globalState = reducer(globalState, action);
7    Object.keys((key) => {
8      if (prevState[key] !== globalState[key]) {
9        listeners[key].forEach(listener => listener());
10      }
11    });
12  };
13  // ...
14  return {
15    useGlobalState,
16    dispatch,
17  };
18};

گام 7: وضعیت همزمان (Concurrent mode)

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

پیاده‌سازی آن کمی پیچیده است، اما در اصل یک قلاب برای ایجاد حالت ایجاد کرده و سپس آن را لینک می‌کنیم.

1const useGlobalStateProvider = () => {
2    const [state, dispatch] = useReducer(patchedReducer, globalState);
3    useEffect(() => {
4      linkedDispatch = dispatch;
5      // ...
6    }, []);
7    const prevState = useRef(state);
8    Object.keys((key) => {
9      if (prevState.current[key] !== state[key]) {
10        // we need to pass the next value to listener
11        listeners[key].forEach(listener => listener(state[key]));
12      }
13    });
14    prevState.current = state;
15    useEffect(() => {
16      globalState = state;
17    }, [state]);
18  };

patchedReducer برای این که setGlobalState بتواند حالت سراسری را به‌روزرسانی کند، ضروری است. قلاب useGlobalStateProvider باید در یک کامپوننت پایدار از قبیل کامپوننت ریشه اپلیکیشن مورد استفاده قرار گیرد.

توجه کنید که این تکنیک متداولی نیست و ممکن است محدودیت‌هایی داشته باشد. برای نمونه فراخوانی شنونده‌ها در رندر عملاً توصیه نمی‌شود. برای این که از وضعیت همزمان به صورت صحیحی پشتیبانی شود، باید از پشتیبانی core برخوردار باشیم. در حال حاضر قلاب useMutableSource در این RFC (+) پیشنهاد شده است.

سخن پایانی

در این مقاله با بخش عمده پیاده‌سازی حالت سراسری در قلاب‌های ری‌اکت آشنا شدیم. البته کد واقعی در تایپ اسکریپت کمی پیچیده‌تر و شامل getGlobalState برای خواندن حالت سراسری از خارج است و پشتیبانی محدودی برای میان‌افزار ریداکس و DevTools دارد.

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۱ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
better-programming
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *