קורס React 2020 שיעור תרגול: Redux Middlewares


זהו נושא דיון מלווה לערך המקורי שב־https://www.tocode.co.il/bundles/react/lessons/redux-middlewares-lab

היי,
בתרגיל השני, בניתי middleware שקורא לdispatch עם פעולה ששומרת בסטור אובייקט של הstate הנוכחי.
אני מקבל error בכל פעם שיש action:
‘Uncaught RangeError: Maximum call stack size exceeded’.
זה קורה גם כשאני שומר רק את messages וrooms של הstate (ולא את כולו)…

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

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

הצלחתי לתקן את זה, בגדול שכחתי לכתוב בmiddleware “תנאי עצירה” : אם הaction הוא save אז תמשיך ואל תשמור.

עכשיו משום מה, מנגנון הundo עצמו לא טוען את הstate הקודם למרות שהוא נשמר כמו שצריך בstore.

ה reducer נראה בסדר. הדפסת את ה Payload בכניסה לראות שיש שם את הערכים המתאימים? מה התופעה שאתה רואה?

הpayload הוא בדיוק האובייקט של הstate הקודם. התופעה היא שהוא פשוט לא עושה undo. רואים עדיין את הstate הנוכחי. למרות ההשמה לstate הקודם בaction.

במצב כזה אני אוהב לפתוח את תוסף כלי הפיתוח:

הוא מראה לך לייב על המסך מה הסטייט ומה כל Action שהגיע.

שאלה ראשונה שצריך לבדוק אחרי זה היא האם יש אי תאימות בין ה State הנוכחי למה שרואים על המסך? כלומר האם ה State ב Redux Store לא משתקף בקומפוננטות שעל המסך?

  1. אם התשובה חיובית צריך לבדוק למה קומפוננטות לא התעדכנו - האם יש כפל של מידע ב State? האם קומפוננטה מסתכלת על השדה הלא נכון? הייתי כותב קומפוננטת בדיקה שרק מציגה את המידע מהסטייט ורואה אם היא מתעדכנת וכך הלאה.

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

בכל מקרה התוסף הזה ייתן לך המון מידע ושווה להתקין אותו וגם להתאמן על העבודה איתו

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

הי,

מה הכוונה “לא כפונקציה” ? כל מידלוור הוא פונקציה.

בשאלה הראשונה בתרגיל אנחנו רוצים שהמידלוור יפעיל setTimeout אם הוא רואה את השדה meta המתאים וכך בעוד X שניות יפעיל את ה dispatch

שלום,
אשמח להערות על הפתרונות שלי, תודה רבה!

תרגיל 1:

store.js

import { applyMiddleware, createStore } from 'redux';

const initialState = {
    tick: 0,
    message: '',
}

const awaitAction = store => next => action => {
    if (!action.meta || !action.meta.delay) return next(action);
         setTimeout(()=>{
            return next(action);
         }, action.meta.delay);
}

const reducer = produce ((state , action)=>{
    switch(action.type) {
        case 'UPDATE_TICK':
            state.tick = action.payload;
            break;
        case 'SHOW_MESSAGE':
            state.message = action.payload;
            break;
    }
}, initialState);

const store = createStore(reducer , applyMiddleware(awaitAction));
export default store;
window.store = store;

actions.js

export function updateTick(tick , delay) {
    return { type: 'UPDATE_TICK' , payload: tick , meta: { delay: delay } };
}

export function showMessage(message , delay) {
    return { type: 'SHOW_MESSAGE', payload: message, meta: { delay: delay } };
}

main.js

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';

import Timer from './timer';
import ShowHideMessage from './showOrHideMessage';

import store from './redux/store';

const App = () => {
    return (
        <Provider store = {store}>
            <Timer/>
            <ShowHideMessage/>
        </Provider>
    );
};

ReactDOM.render(<App/> , document.querySelector('main'));

timer.js

import React, { useEffect } from 'react';
import { connect } from 'react-redux';

import { updateTick } from './redux/actions';

function mapStateToProps(state) {
    return {
        tick: state.tick,
    };
}

export default connect(mapStateToProps)(function Timer({tick  , dispatch}) {

    useEffect(()=>{
        dispatch(updateTick(tick + 1 , 1000));
    },[tick]);


    return (
        <p>Ticks : {tick}</p>
    );
})

showOrHideMessage.js

import React from 'react';
import { connect } from 'react-redux';

import { showMessage } from './redux/actions';

function mapStateToProps(state) {
    return {
       message: state.message,
    }
}

export default connect(mapStateToProps)(function ShowHideMessage({message , dispatch}) {

    function showOrHideMessageHandler() {
        dispatch(showMessage(message? '' : 'Hello world' , 1000));
    }

    return(
        <>
            <h1>Message:</h1>
            <p>{message}</p>
            <button onClick={showOrHideMessageHandler}>Show / hide message</button>
        </>
    );
});

תרגילים 2/3

store.js

import produce from 'immer';
import { applyMiddleware, createStore } from 'redux';
import { undoRedoMiddlaWare , saveToLocalStorage , initialState} from './middleWares';

const reducer = produce ((state , action)=>{
   switch(action.type) {
       case 'UPDATE_INPUT':
            state[action.payload.id] = action.payload.value;
            break;
       case 'GET_PRE_STATE':
            return action.payload;
       case 'GET_POST_STATE':
            return action.payload;
   }
}, initialState);

const store = createStore(reducer ,applyMiddleware(undoRedoMiddlaWare , saveToLocalStorage));
export default store;
window.store = store;

middleWares.js

export const initialState = JSON.parse(localStorage.getItem('state')) || {
    userName: '',
    password: '',
}

const states = [initialState];
let indexOfCurrentState = 0;

export const undoRedoMiddlaWare = ({dispatch , getState}) => next => action => {
    const nextAction = next(action);
    switch(action.type) {
        case 'UNDO':
            states[indexOfCurrentState-1] && dispatch({type: 'GET_PRE_STATE' , payload:states[--indexOfCurrentState]});
            break;
        case 'REDO':
            states[indexOfCurrentState+1] && dispatch({type: 'GET_POST_STATE' , payload:states[++indexOfCurrentState]});
            break;            
    }
    !(/PRE|POST|UNDO|REDO/g).test(action.type) && states.push(getState()) && ++indexOfCurrentState;
    return nextAction;
}

export const saveToLocalStorage = ({dispatch , getState}) => next => action => {
    next(action);
    localStorage.setItem('state' , JSON.stringify(getState()));
}

actions.js

export function updateInput(id , value) {
    return ({ type: 'UPDATE_INPUT' , payload: {id , value}});
}

export function undo() {
    return ({type: 'UNDO'});
}

export function redo() {
    return ({type: 'REDO'});
}

main.js

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';

import store from './redux/store';
import Form from './form';
import '../styles/main.scss';

const App = () => {
    return (
        <Provider store = {store}>
           <Form/>
        </Provider>
    );
};

ReactDOM.render(<App/> , document.querySelector('main'));

form.js

import React from 'react';
import { connect } from 'react-redux';

import { updateInput , undo, redo} from './redux/actions';

function mapStateToProps(state) {
    return { 
        userName: state.userName,
        password: state.password,
    }
}

export default connect(mapStateToProps)(function Form({userName , password , dispatch}){

    function onInputHandler(e) {
        dispatch(updateInput(e.target.id , e.target.value));
    }

    return (
        <form autoComplete="on">
          <h1>Login Page</h1>
          <div className="form-outline mb-4">
              <label className="form-label" htmlFor="userName">User Name:</label>
              <input className="form-control" type="text" id="userName" value={userName} onInput={(e)=>onInputHandler(e)}/>
          </div>
          <div className="form-outline mb-4">
              <label htmlFor="password">Password:</label>
              <input className="form-control" type="password" id="password" value={password} onInput={(e)=>onInputHandler(e)}/>
          </div>
          <button onClick={(e)=>{  e.preventDefault(); dispatch(undo()); }}>Undo</button>
          <button onClick={(e)=>{  e.preventDefault(); dispatch(redo()); }}>Redo</button>
        </form>
      );
});
לייק 1

נראה מעולה

אני רואה ששמרת את ה States הישנים בתוך משתנה גלובאלי. זה בסדר כאן אבל הרבה פעמים במקרה הכללי בפיצ’רים כאלה אנשים פותחים עוד ענף בסטייט הגלובאלי (מה Redux Store) ובו שומרים את כל הסטייטים הישנים.

שווה להסתכל גם בקוד הספריה כאן כדי לקבל רעיונות (לא רק בתיעוד אלא ממש להיכנס לקוד שלהם לראות שמבינים איך זה בנוי):

לייק 1

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

React.cloneElement(React.Children.toArray(props.children)[currentFormIndex]);

או שמספיק לקחת את props.children[currentFormIndex]
כי הרי בטופס שמנוהל על ידי redux אין צורך להעביר props לטפסים.

פקד המיכל שלי נראה כך:

import React from 'react' ;
import { connect } from 'react-redux';

import { previousBtnClick , nextBtnClick } from './redux/actions';

function mapStateToProps(state) {
    return {
        currentFormIndex: state.currentFormIndex,
    };
}

export default connect(mapStateToProps)(function FormsContainer(props) {
    const { currentFormIndex , dispatch} = props;
    const countOfPages = React.Children.count(props.children);
/*
    function getCurrentPage() {
       return React.cloneElement(React.Children.toArray(props.children)[currentFormIndex]);
    }
*/
    return (
        <div>
            {props.children[currentFormIndex]}
            <button onClick={()=>dispatch(previousBtnClick())} disabled={currentFormIndex===0}>Previous</button>
            <button onClick={()=>dispatch(nextBtnClick())} disabled={currentFormIndex===countOfPages-1}>Next</button>
        </div>
    );
});

הי

אני חושב שבתרגיל הזה לא צריך להשתמש ב cloneElement בכל מקרה (גם בלי רידאקס). למה בחרת להשתמש בפונקציה זו?

בלי רידאקס הקומפוננטה הזו אמורה לשמור את כל המידע עבור הילדים שלה ולהעביר אותם כ-props, כיצד ניתן להעביר props בלי ה-cloneElement?
לעומת רידאקס ששם כל הילדים מקבלים את ה-data מה-store ומעדכנים אותם עם dispatch.
כך זה נראה בלי רידאקס:

import React, { useEffect, useState } from 'react';

export function FormsContainer(props) {
    const [dataObjOfAllPages , setDataObjOfAllPages] = useState({});
    const [currentIndex , setCurrentIndex] = useState(0);
    const countOfPages = React.Children.count(props.children);
  
    function updateDataObjOfAllPages(dataObj) {
      setDataObjOfAllPages({...dataObjOfAllPages, ...dataObj});
    }
    
    function getCurrentPage() {
      const child = React.Children.toArray(props.children)[currentIndex];
      return React.cloneElement(child , { dataObjOfAllPages : {...child.props.dataObjOfAllPages , ...dataObjOfAllPages} , updateDataObjOfAllPages});
    }
   
    return (
      <>
        {getCurrentPage()}
        <div className="btnsContainer">
            <button disabled={currentIndex === 0} onClick={()=>setCurrentIndex(v=>v-1)}>Previous</button>
            <button disabled={currentIndex === countOfPages-1} onClick={()=>setCurrentIndex(v=>v+1)}>Next</button>
        </div>
      </>
    );
  }

הי ינון
בקשר ל UNDO
אשמח לתגובתך על הדרך שבה מימשתי
הלכתי לכיוון שבו כל פעולה מחזירה גם את ה UNDO שלה
למשל
export function receivedMessage(message) {
const undoData = {type: ‘DELETE_MESSAGE’, payload: message.id}
const resultData = {type: ‘RECEIVED_MESSAGE’, payload: message }
return {result: resultData, undo: undoData};}

והוספתי reducer שנקרא UNDO שכל מה שהוא עושה זה שומר ב
const initialState = {
lastaction: {},
};

ככה ש dispatcher שקורא ל state.undo.lastaction בעצם ישר עושה את הפעולה ההפוכה לפעולה האחרונה

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

לייק 1

אני מנסה לפתור את תרגיל 2,
כרגע אני מתייחסת לביטול פעולה אחרונה אחת.
הקוד שלי נראה כך:
store.js:

import { configureStore } from "@reduxjs/toolkit";
import studentsReducer from "./slices/students";
import babiesReducer from "./slices/babies";

// babies middleware
const undoMiddleware = ({ dispatch, getState }) => next => action => {   
    if(action.type === 'babies/UNDO'){
        dispatch({type: 'babies/UNDOO', payload: lastState})
    }
    else if(action.type === 'babies/UNDOO'){
        next(action);  
    }
    else{
        lastState = getState();
        console.log('lastState', lastState)
        next(action);
    }
}


const store = configureStore({
    reducer: {
        students: studentsReducer,
        babies:  babiesReducer   
    },

    middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(delayMiddleWare, undoMiddleware)
})

let lastState = store.getState();


export default store;

babies.js:

import { createSlice } from "@reduxjs/toolkit"

const initialState = {
    babies: [
        { id: 1, name: 'Yael' },
        { id: 2, name: 'Rachel' },
    ],
}



export const slice = createSlice({
    name: 'babies',
    initialState,
    reducers: {
        addBaby(state, action) {
            state.babies.push(action.payload)
        },
        removeBaby(state, action) {
            state.babies = state.babies.filter(b => b.id != action.payload.id)
        },
        UNDO(state, action) {

        },
        UNDOO(state, action) {
            
        }
    }
})

export default slice.reducer;
export const { addBaby, removeBaby, UNDO } = slice.actions;

אני תקועה בפונקציה UNDOO- איך לעדכן את הסטייט למה שהגיע בpayload?
ובאופן כללי- האם הדרך הזו טובה?

הי,

שימי לב שהממשק של Reducer מאפשר שני דברים: אופציה אחת זה לעדכן את ה state כמו שעשית ב addBaby ו removeBaby. אופציה שניה (יותר רלוונטית ל undo) זה להחזיר אוביקט סטייט חדש לגמרי, לדוגמה אם הייתי רוצה לטפל בפעולה של reset הייתי יכול ליצור reducer כזה:

reset(state, action) { return initialState }

ואותו דבר לגבי ה Undo. במקום לעדכן את state פשוט מחזירים את ה state הקודם שהוא קיבל דרך ה action