• בלוג
  • יבוא מעגלי ו Redux Tolkit

יבוא מעגלי ו Redux Tolkit

10/04/2023

אם תתחילו לעבוד עם Redux Toolkit לפרויקט משמעותי ותכתבו את הקוד לפי הדוגמאות שבאתר התיעוד, סיכוי טוב שתסיימו עם הצבעות מעגליות. בואו נראה למה ואיך להימנע מזה.

1. מה הבעיה עם הצבעה מעגלית

הצבעה מעגלית היא פשוט מצב בו מודול אחד ב JavaScript מייבא מודול שני, ואותו מודול שני גם הוא מייבא את המודול הראשון (לפעמים דרך יבוא מודולים נוספים). הנה דוגמה פשוטה עם שני מודולים:

// file a.js
import b from './b.js';

export const text = "hello world";

function go() {
  b();
}

go();
// file b.js

import { text } from './a.js';

export default function b() {
  console.log(text);
}

וזה עובד, אבל מוזר. כי עכשיו אם אני משנה את הקובץ b.js לקוד הבא:

import { text } from './a.js';

console.log(`using text - ${text}`);
export default function b() {
  console.log(text);
}

ומריץ את a.js אני כבר מקבל את השגיאה:

console.log(`using text - ${text}`);
                            ^

ReferenceError: Cannot access 'text' before initialization
    at file:///Users/ynonp/tmp/blog/circular/b.js:3:29
    at ModuleJob.run (node:internal/modules/esm/module_job:194:25)

בשביל להבין מה קרה שם צריך להתחיל עם a - הוא מתחיל בייבוא הפונקציה b מתוך המודול b.js ורק אחרי היבוא הוא מייצא את הטקסט. לכן למרות ש b מדפיס את הטקסט אחרי היבוא, בעצם אנחנו במצב של יבוא חלקי. הצלחנו לייבא את השם text אבל המודול a עוד לא התחיל לרוץ ולכן אין לו עדיין ערך.

ואם בדוגמה כל כך פשוטה אפשר להסתבך, רק דמיינו את השגיאות והנפילות שיקבלו את פניכם בדוגמאות יותר מסובכות מפרויקטים אמיתיים.

2. מה הבעיה עם Redux Toolkit

דוגמת הקוד הראשונה מ Redux Toolkit בדף ה Quickstart שלהם היא:

import { createSlice } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'

export interface CounterState {
  value: number
}

const initialState: CounterState = {
  value: 0,
}

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => {
      // Redux Toolkit allows us to write "mutating" logic in reducers. It
      // doesn't actually mutate the state because it uses the Immer library,
      // which detects changes to a "draft state" and produces a brand new
      // immutable state based off those changes
      state.value += 1
    },
    decrement: (state) => {
      state.value -= 1
    },
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload
    },
  },
})

// Action creators are generated for each case reducer function
export const { increment, decrement, incrementByAmount } = counterSlice.actions

export default counterSlice.reducer

הקוד מראה לנו איך ליצור Actions באמצעות הגדרת מפתחות באוביקט reducers, ומסביר שאנחנו מקבלים במתנה Action Creators אותם אפשר לייצא מהסלייס. כל עוד רק קומפוננטות ריאקט וה Store יהיו תלויים בסלייס הזה אין שום בעיה.

עכשיו בואו נמשיך עם הדוגמה שלהם כדי לכתוב Async Thunk, למשל ארצה לכתוב פעולה אסינכרונית שאחרי שניה מעלה את ה counter. מעמוד הדוגמה של createAsyncThunk אני לומד שאני יכול לכתוב את הקוד הבא כדי לייצר async thunk:

export const incrementLater = createAsyncThunk(
  'todos/incLater',
  // Declare the type your function argument here:
  async (_, {dispatch, getState}) => {
    // PROBLEM - getState() depends on store

    await new Promise((resolve, reject) => setTimeout(resolve, 1000));
    dispatch(increment());
  }
)

וזה כבר מתחיל להיות מסוכן - כי קוד של Async Thunk תלוי ב State הראשי של ה Store (זה מה ש getState מחזיר), אבל ה Store תלוי בקובץ של הסלייס (כדי ליצור את ה Store), וכך אם אני כותב את הקוד באותו קובץ של הסלייס יצרתי מעגל.

3. הפיתרון: שימו לב מה תלוי במה

המפתח לשבירת מעגלי תלויות הוא לזהות מה צריך להיות תלוי במה, ולחלק את הקוד לקבצים בצורה היררכית. במקרה של Redux Toolkit אנחנו רוצים לייצר את היררכיית התלויות הבאה:

  1. קבצי Action (מכילים קריאות ל createAction) - לא תלויים בכלום.

  2. קבצי Slice תלויים בקבצי Action.

  3. קובץ ה Store תלוי בקבצי ה Slice ואולי בקבצי ה Action.

  4. קבצי Selectors תלויים בקובץ ה Store.

  5. קבצי ה Thunk תלויים בקובץ ה Store וגם הרבה פעמים ב Selectors.

בשביל זה רוב הזמן לא נרצה להגדיר את ה Actions שלנו בתוך ה Reducers, אלא להשתמש במפתח extraReducers לדוגמה:

import addTodo from "./addTodo";

extraReducers: (build) => {
  build.addCase(addTodo, (state, action) => {
    state.push({ id: nanoid(), message: action.payload, completed: false });
  });
}

והתלות עכשיו היא לפי ההיררכיה שקבענו - סלייס מייבא Action Creator. סלייסים נוספים או Async Thunks יוכלו גם לייבא את addTodo בלי ליצור מעגליות.

לסיכום אם אתם עובדים עם Redux Toolkit אלה הנקודות שצריך לשים אליהן לב במיוחד:

  1. תלויות בין סלייסים - כשסלייסים מתחילים לייבא Actions שנוצרו בסלייסים אחרים מהר מאוד נגיע למעגלים של סלייסים. הפיתרון יהיה תמיד להוציא את ה Action Creators לקבצים נפרדים ולוודא שהם לא תלויים בשום רכיב אחר.

  2. תלויות בין Thunks ל Action Creators - הקפידו להפריד את ה Thunks וה Action Creators לקבצים שונים. ה Action Creators הרגילים לא תלויים בכלום, אבל Async Thunks תלויים בהכל.

  3. הרבה פעמים משתלם להוציא הגדרות Types לקבצי הגדרות נפרדים, כי יבוא של הגדרות טיפוסים גם עלול ליצור מעגלים.

עבודה עם Redux Toolkit דורשת משמעת, אבל כל עוד אנחנו מקפידים על חלוקה היררכית ויבוא רק של דברים מסוגים בסיסיים יותר אפשר לבנות גם פרויקטים גדולים ללא מעגלים ולחסוך הרבה כאבי ראש.