הפונקציות useContext ו useReducer ב React Hooks


המעבר ל React Hooks מצריך לימוד של תבניות חדשות כדי לקודד את הקשר בין פקדי ריאקט (React Components) שונים. בוובינר זה ראינו תבנית לניהול State בפקדים פונקציונאליים באמצעות useReducer ושיתוף ה State עם הילדים.

1. מתי נשתמש בתבנית

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

2. מה עושים

השימוש ב useReducer יאפשר לנו להגדיר את ה State של הפקד במקום אחד בפקד האב, ולהעביר את הסטייט כולו או חלקים ממנו לילדים. בנוסף במקום להעביר מספר פונקציות לשינוי הערך אנחנו נעביר רק פונקציה אחד בשם dispatch, שמקבלת כפרמטר את סוג השינוי שהילד רוצה לבצע.

באמצעות useContext נוכל להעביר את פונקציית ה dispatch באופן אוטומטי בלי לציין אותה בתור Property בכל היררכיית הפקדים. כל ילד שירצה לשנות את המידע שנמצא באב ימשוך מה Context את dispatch ויפעיל אותה עם הפרמטרים המתאימים.

הפונקציה useReducer דורשת שנגדיר מבעוד מועד פונקציה שמתארת את כל שינויי המצב האפשריים במידע שהיא מנהלת. לדוגמא אם המידע כולל פרטי איש קשר נוכל לדמיין פונקציית reducer שתיראה כך:

function reducer(state, action) {
    switch(action.type) {
        case 'SET_NAME':
            return { ...state, name: action.payload };

        case 'SET_EMAIL':
            return { ...state, email: action.payload };

        default:
            return state;
    }
}

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

אחרי שהגדרנו את הפונקציה reducer השימוש בה מתוך קוד פקד נראה כך:

function App(props) {
    const [state, dispatch] = useReducer(reducer, { name: 'test', email: 'test@gmail.com' });
}

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

3. דוגמת קוד

דוגמת הקוד שהוצגה בוובינר זמינה בקישור הבא:

https://codesandbox.io/s/pedantic-field-u7vmo

בנוסף אני מדביק כאן את הקובץ הראשי שהוצג:

import React from "react";
import ReactDOM from "react-dom";
import { useState, useReducer, useContext } from "react";
import _ from "lodash";
import "./styles.css";

const dispatchContext = React.createContext(() => {});

const initialCounters = [
  { val: 3, delta: 2 },
  { val: 5, delta: 1 },
  { val: 1, delta: 3 }
];

function reducer(state, { type, payload }) {
  switch (type) {
    case "inc":
      const newCounters = _.cloneDeep(state);
      newCounters[payload].val += newCounters[payload].delta;
      return newCounters;

    case "dec":
      const newCounters2 = _.cloneDeep(state);
      newCounters2[payload].val -= newCounters2[payload].delta;
      return newCounters2;

    default:
      return state;
  }
}

function App() {
  const [counters, dispatch] = useReducer(reducer, initialCounters);

  const maxCounter = Math.max(...counters.map(i => i.val));
  return (
    <dispatchContext.Provider value={dispatch}>
      <div className="App">
        <h2>Counter Demo</h2>

        <p>The largest counter value is: {maxCounter}</p>
        <CounterBox counters={counters} />
      </div>
    </dispatchContext.Provider>
  );
}

function CounterBox(props) {
  return (
    <>
      {props.counters.map((counter, id) => (
        <Counter value={counter.val} delta={counter.delta} id={id} />
      ))}
    </>
  );
}

function Counter(props) {
  const dispatch = useContext(dispatchContext);
  return (
    <div className="Counter">
      <p>
        Counter: {props.value}, delta: {props.delta}
        <button
          onClick={() => {
            dispatch({ type: "inc", payload: props.id });
            // props.dispatch({ type: "inc", payload: props.id });
          }}
        >
          +
        </button>
        <button onClick={() => dispatch({ type: "dec", payload: props.id })}>
          -
        </button>
      </p>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

4. חלופות

האלטרנטיבה המרכזית ל useReducer היא useState. ההבדל המרכזי בין השתיים נוגע למבנה הקוד שיווצר:

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

באמצעות useReducer אנחנו מגדירים את לוגיקת השינוי בסטייט ולכן נעדיף להשתמש בה כשיש לנו היררכיית פקדים יותר גדולה ואנחנו צריכים לחלק את השליטה במידע בין מספר פקדים. פקדי הילדים יוכלו "לבקש" שינויים באמצעות קריאה ל dispatch, והלוגיקה של השינוי ומה בדיוק יישמר נכתבת בפונקציה reducer.