iamjpcbau
iamjpcbau

Reputation: 404

React.js: Why is my variable in useReducer empty?

I'm trying to pass an object containing array to a child component

I've implemented some loading until my state array called profileModule is populated

However, the object 'profileModule' being passed contains an empty array

Can anyone enlighten me on what am I missing? Please see code below.

Actual passing of object to child component:

var [state, dispatch] = useReducer(layoutReducer, {
    isSidebarOpened: true,
    modules: profileModules,
  });

  if (profileModules && profileModules.length > 0) {
    return (
      <LayoutStateContext.Provider value={state}>
        <LayoutDispatchContext.Provider value={dispatch}>
          {children}
        </LayoutDispatchContext.Provider>
      </LayoutStateContext.Provider>
    );
  } else {
    return <LoadingOverlay active={true} spinner={<FadeLoader />} text="" />;
  }

As you can see above, in useReducer function, I'm trying to pass modules: profileModules to a child component

I don't understand why I'm getting value when I console.log it before the actual rendering of page but when its inside useReducer I only get empty array

Here is how I populate the value of profileModule which comes from an API

useLayoutEffect(() => {
    retrieveProfileDetails();
  }, []);

  useLayoutEffect(() => {
    if (profileDetails.length > 0) {
      const modules = profileDetails.map((module) => module.module);
      setProfileModules(profileModules.concat(modules));
    }
  }, [profileDetails]);

  const retrieveProfileDetails = useCallback(() => {
    const profileListArr = profileList.split(",");
    profileListArr.forEach((profileListArrMap) => {
      ProfileMaintenanceService.retrieveProfileDetails(profileListArrMap).then(
        (response) => {
          setProfileDetails(response.data);
        }
      );
    });
  });

Full code:

import React, {
  createContext,
  useReducer,
  useContext,
  useState,
  useEffect,
  useLayoutEffect,
  useCallback,
} from "react";
import { USER_PROFILE_ID_SESSION_ATTRIBUTE } from "../services/AuthenticationService";
import ProfileMaintenanceService from "../services/ProfileMaintenanceService";
import LoadingOverlay from "react-loading-overlay";
import FadeLoader from "react-spinners/FadeLoader";

var LayoutStateContext = createContext();
var LayoutDispatchContext = createContext();

export {
  LayoutProvider,
  useLayoutState,
  useLayoutDispatch,
  toggleSidebar,
  toggleOffSidebar,
};

function toggleSidebar(dispatch) {
  dispatch({
    type: "TOGGLE_SIDEBAR",
  });
}

function toggleOffSidebar(dispatch) {
  dispatch({
    type: "TOGGLE_OFF_SIDEBAR",
  });
}

function layoutReducer(state, action) {
  switch (action.type) {
    case "TOGGLE_SIDEBAR":
      return { ...state, isSidebarOpened: !state.isSidebarOpened };
    case "TOGGLE_OFF_SIDEBAR":
      return { ...state, isSidebarOpened: false };
    case "CUSTOMIZE_SIDEBAR":
      return { ...state, isSidebarOpened: false };
    default: {
      throw new Error(`Unhandled action type: ${action.type}`);
    }
  }
}

function LayoutProvider({ children }) {
  const profileList = sessionStorage.getItem(USER_PROFILE_ID_SESSION_ATTRIBUTE);

  const [profileDetails, setProfileDetails] = useState([]);
  const [profileModules, setProfileModules] = useState([]);

  const [loading, setLoading] = useState(true);

  useLayoutEffect(() => {
    retrieveProfileDetails();
  }, []);

  useLayoutEffect(() => {
    if (profileDetails.length > 0) {
      const modules = profileDetails.map((module) => module.module);
      setProfileModules(profileModules.concat(modules));
    }
  }, [profileDetails]);

  const retrieveProfileDetails = useCallback(() => {
    const profileListArr = profileList.split(",");
    profileListArr.forEach((profileListArrMap) => {
      ProfileMaintenanceService.retrieveProfileDetails(profileListArrMap).then(
        (response) => {
          setProfileDetails(response.data);
        }
      );
    });
  });

  var [state, dispatch] = useReducer(layoutReducer, {
    isSidebarOpened: true,
    modules: profileModules,
  });

  if (profileModules && profileModules.length > 0) {
    return (
      <LayoutStateContext.Provider value={state}>
        <LayoutDispatchContext.Provider value={dispatch}>
          {children}
        </LayoutDispatchContext.Provider>
      </LayoutStateContext.Provider>
    );
  } else {
    return <LoadingOverlay active={true} spinner={<FadeLoader />} text="" />;
  }
}

function useLayoutState() {
  var context = useContext(LayoutStateContext);
  if (context === undefined) {
    throw new Error("useLayoutState must be used within a LayoutProvider");
  }
  return context;
}

function useLayoutDispatch() {
  var context = useContext(LayoutDispatchContext);
  if (context === undefined) {
    throw new Error("useLayoutDispatch must be used within a LayoutProvider");
  }
  return context;
}

Upvotes: 0

Views: 464

Answers (1)

Drew Reese
Drew Reese

Reputation: 202731

Issue

The initial modules state is [] from const [profileModules, setProfileModules] = useState([]); higher up in the component. The useLayoutEffect that setProfileModules(profileModules.concat(modules)); updates the value of profileModules for the next render cycle. In the first render cycle when the component mounts the value of profileModules is only ever able to be what the initial state is from the useState hook.

Solution

Add a new action creator and case to handle updating the modules state value when profileModules will update

function updateModules(dispatch, modules) {
  dispatch({
    type: "UPDATE_MODULES",
    modules,
  });
}

function layoutReducer(state, action) {
  switch (action.type) {
    case "TOGGLE_SIDEBAR":
      return { ...state, isSidebarOpened: !state.isSidebarOpened };
    case "TOGGLE_OFF_SIDEBAR":
      return { ...state, isSidebarOpened: false };
    case "CUSTOMIZE_SIDEBAR":
      return { ...state, isSidebarOpened: false };

    case "UPDATE_MODULES":
      return { ...state, modules: action.modules }; // <-- reduce new modules into state

    default: {
      throw new Error(`Unhandled action type: ${action.type}`);
    }
  }
}

Update the useLayoutEffect that triggers on profileDetails updating to dispatch updateModules

useLayoutEffect(() => {
    if (profileDetails.length > 0) {
      const modules = profileDetails.map((module) => module.module);
      setProfileModules(profileModules.concat(modules));

      updateModules(dispatch, modules); // <-- update modules
    }
  }, [dispatch, profileDetails]);

Upvotes: 1

Related Questions