היכרות עם Redux Toolkit

27/06/2022

פוסט זה כולל טיפ קצר לעבודה עם React. אם אתם רוצים ללמוד איתי ריאקט מההתחלה ובצורה מקצועית תשמחו לשמוע שבניתי קורס מלא הכולל עשרות שיעורי וידאו והמון תרגול בו לומדים ריאקט מההתחלה ועד לנושאים המתקדמים.
לפרטים נוספים והרשמה בקרו בדף קורס ריאקט כאן באתר.
 

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

1. הסלייס הראשון שלי

לרידאקס יש Reducers, ופונקציה שכולם משתמשים בה שנקראת combineReducers. נהוג גם להגדיר פונקציות שנקראות Action Creators שמחזירות אוביקט Action, כלומר כזה שיש לו type ו payload.

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

בשביל הדוגמה אני אבנה משחק Game Of Life. המשחק מורכב מלוח אינסופי דו-מימדי בו כל משבצת יכולה להיות "חיה" או "מתה", וכל תור כל המשבצות משתנות לפי הכללים הבאים:

  1. משבצת חיה עם 2 או 3 שכנים חיים נשארת בחיים.

  2. משבצת ריקה שמסביבה יש 3 משבצות חיות תהפוך ל"חיה".

  3. כל שאר המשבצות הופכות מתות.

לאורך זמן אם מתחילים מתבניות מעניינות המשחק יכול לייצר ציורים יפים ויש כל מיני מחקרים סביבו. אפשר וכדאי לקרוא בויקיפדיה: https://en.wikipedia.org/wiki/Conway%27sGameof_Life

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

  1. מצב התחלתי לחלק הספציפי של העץ שמתאים לסלייס.

  2. רשימה של פונקציות שיודעות לקבל את מצב תת העץ ואוביקט action ולהחזיר את המצב המעודכן.

גם בלי Redux Toolkit היינו מפצלים את אוביקט ה Reducer הגדול לסלייסים. רידאקס טולקיט נותן קיצורי דרך יעילים.

הבסיס של קובץ סלייס הוא בסך הכל קריאה לפונקציה createSlice של Redux Toolkit:

export const counterSlice = createSlice({
  name: 'gol',
  initialState,
  reducers: {
    step,
    undo,
  },
})

// Action creators are generated for each case reducer function
export const actions = counterSlice.actions

export default counterSlice.reducer

הפונקציה מקבלת את שם הסלייס, את המצב הראשוני שלו ואת הפונקציות שצריכות להגיב ל Actions בסלייס הזה. אחרי שבנינו אותו אפשר לייצא ממנו את ה reducer ואת ה Action Creators.

במימוש שלי של Game Of Life זה קובץ הסלייס המלא שכתבתי:

import { createSlice } from '@reduxjs/toolkit'

const initialState = {
  world: {
    [[0, -1]]: 1,
    [[0, 0]]: 1,
    [[0, 1]]: 1,
  },
  steps: [],
}

function getNeighborsPositions(pos) {
  const [row, column] = pos.match(/([-\d]+)/g).map(x => Number(x));
  return [
    [row-1, column-1], [row-1, column], [row-1, column+1],
    [row,   column-1],                  [row,   column+1],
    [row+1, column-1], [row+1, column], [row+1, column+1],
  ];
}


function getLiveNeighborsCount(world, pos) {
  return getNeighborsPositions(pos)
    .map(([r, c]) => world[[r, c]])
    .filter(x => x)
    .length;
}

function nextState(world) {
  const next = {};

  for (let [pos, value] of Object.entries(world)) {
    const liveNeighbors = getLiveNeighborsCount(world, pos);
    if (
      (value && (liveNeighbors === 2 || liveNeighbors === 3)) ||
      (!value && liveNeighbors === 3)) {
      next[pos] = 1
    }
    for (let np of getNeighborsPositions(pos)) {
      if (!world[np] && getLiveNeighborsCount(world, np.toString()) === 3) {
        next[np] = 1;
      }
    }
  }
  return next;
}

function step(state) {
  state.steps.push(state.world);
  state.world = nextState(state.world);
}

function undo(state) {
  const lastState = state.steps.pop();
  state.world = lastState;
}

export const counterSlice = createSlice({
  name: 'gol',
  initialState,
  reducers: {
    step,
    undo,
  },
})

// Action creators are generated for each case reducer function
export const actions = counterSlice.actions

export default counterSlice.reducer

שווה להתמקד באחת הפונקציות של הטיפול ב Action, למשל undo:

function undo(state) {
  const lastState = state.steps.pop();
  state.world = lastState;
}

זה מוזר! אני משתמש ב pop ובהשמה לשדה ב state, ממש כאילו המשתנים היו Mutable Data רגיל של JavaScript. למעשה, רידאקס טולקיט משתמש ב immer מאחורי הקלעים כדי לאפשר לי לכתוב קוד כמו שאני רגיל ולקבל תוצאה כמו שרידאקס אוהב.

2. ה Store הראשון שלי

מבחינת כתיבת ה Store הקוד נראה דומה ל Redux אבל יש פה כמה פינוקים. זה הקוד של ה Store במשחק שלי:

import { configureStore } from '@reduxjs/toolkit'
import golReducer from './features/gol/golSlice'

export function createStore() {
  return configureStore({
    reducer: {
      gol: golReducer,
    },
  })
}

export const store = createStore();

בגיזרת הפינוקים אנחנו מקבלים תמיכה אוטומטית ב thunk ובסביבת הפיתוח בכלי הפיתוח של רידאקס. אפשר גם להוסיף בקלות מידלוורים משלנו.

3. גישה למשתנים ולפעולות מתוך ריאקט

מבחינת הגישה מתוך ריאקט אנחנו ממשיכים להשתמש בספריית react-redux בדיוק כמו שהיינו עושים בלי redux-toolkit. זה אומר שאיפשהו במעלה העץ אני צריך להדביק את ה store בתוך אלמנט Provider, למשל בקובץ index.js:

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { store } from "./dl/store";
import { Provider } from "react-redux";

import App from "./App";

const rootElement = document.getElementById("root");
const root = createRoot(rootElement);

root.render(
  <StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </StrictMode>
);

ובקובץ App.js אני יכול להשתמש בכל ה hooks הרגילים של react-redux באופן הבא:

import "./styles.css";
import _ from "lodash";

import { useSelector, useDispatch } from "react-redux";
import { actions } from "./dl/features/gol/golSlice";
const { step, undo } = actions;

function App() {
  const world = useSelector((state) => state.gol.world);
  const canUndo = useSelector((state) => state.gol.steps.length);
  const dispatch = useDispatch();

  return (
    <>
      <div className="buttons">
        <button onClick={() => dispatch(step())}>Step</button>
        <button onClick={() => dispatch(undo())} disabled={!canUndo}>
          Undo
        </button>
      </div>
      <div className="world">
        {_.range(-10, 10).map((row) =>
          _.range(-10, 10).map((col) => (
            <div
              key={`${row}-${col}`}
              className={`cell ${world[[row, col]] ? "alive" : "dead"}`}
            />
          ))
        )}
      </div>
    </>
  );
}

export default App;

סך הכל הבונוס הגדול של redux-toolkit הוא הסלייסים והיצירה האוטומטית של ה Action Creators. זה אולי לא נשמע המון כרגע, אבל הכתיב הסטנדרטי של Reducers יחד עם תמיכה מובנית ב Immer הופכים את רידאקס להרבה יותר נגיש ומשאירים הרבה פחות מקום לטעויות.

אלה מכם שירצו לשחק עם אפליקציית ה Game Of Life שלי מוזמנים לבקר אותה בקוד סנדבוקס הבא: https://codesandbox.io/s/strange-pascal-bhtxnw

ואם Redux Toolkit נשמעה לכם מעניינת ותרצו לשמוע עליה יותר שווה לבקר באתר שלהם כאן: https://redux-toolkit.js.org/