0
votes

I've got a custom hook using useReducer that's controlling which components are rendered on a dashboard. It works as expected in the parent, but when I'm using it in a child, useReducer runs, state is changed, but it's not changing in parent component, so it's not re-rendering with the appropriate changes. I'm using the spread operator in my reducer to return a new object. I've tried several hacky things with extra useStates and useEffects inside the hook but none of them have had an impact. I've tried different levels of destructuring, also no effect. I can see the state being changed but it seems like it's not being recognized as a new object when being returned inside the parent object.

Custom Hook

import { useReducer } from "react"

let dashboard = {}

export const dashboardReducer = (state, action) => {
    console.log("dashboardReducer: state, action=", state, action)
    switch (action.component) {
        case 'lowerNav':
            switch (action.label) {
                case 'journal':
                return { ...state, lowerNav: 'journal'}
                case 'progress':
                return { ...state, lowerNav: 'progress'}
                case 'badges':
                return { ...state, lowerNav: 'badges'}
                case 'challenges':
                return { ...state, lowerNav: 'challenges'}
                case 'searchResults':
                return { ...state, lowerNav: 'searchResults'}
            }
        case 'foodSearchBox' :
            if (action.searchResults) {
                return { ...state, searchResults: action.searchResults, lowerNav: "searchResults"}
            } else {
                return { ...state, searchResults: "NO SEARCH RESULTS"}                
            }
        default:
            throw new Error()
    }
}

export const useDashboard = () => {
    const [ state, dispatch ] = useReducer(dashboardReducer, dashboard)
    //fires on every dispatch no matter which component
    console.log("useDashboard: state=", state)      
    return [ state, dispatch ]
}

export const initDashboard = initialState => {
    dashboard = initialState
}

Parent Component

const Dashboard = () => {
  initDashboard({ lowerNav: "journal", log: "daily", meal: "breakfast" });
  const [ state, dispatch ] = useDashboard();
  const lowerNav = state.lowerNav

  useEffect(() => {
    console.log("Dashboard: useEffect: state=", state) //Fires when a dispatch is run from this component, but not from any other components
  }, [state])

  const dateOptions = { year: "numeric", month: "long", day: "numeric" }; 
  const currentDate = new Date(Date.now()); 

  return (
    <Layout>
      <div className="flex">
        <DashUser />
        <div className="flex-1"></div>
        <div className="flex-1 px-32 self-center">
          <FoodSearchBox />
        </div>
      </div>
      <nav className="flex bg-mobileFoot">
        <div className="flex-1"></div>
        <ul className="flex-1 flex justify-around text-lg font-medium py-2">
          <li
            className={`${
              lowerNav === "journal" ? "border-b-2 border-pink-500" : ""
            } cursor-pointer`}
            value="journal"
            onClick={() =>
              dispatch({ component: "lowerNav", label: "journal" })
            }
          >
            Food Journal
          </li>
          <li
            className={`${
              lowerNav === "progress" ? "border-b-2 border-pink-500" : ""
            } cursor-pointer`}
            value="progress"
            onClick={() =>
              dispatch({ component: "lowerNav", label: "progress" })
            }
          >
            Progress
          </li>
          <li
            className={`${
              lowerNav === "badges" ? "border-b-2 border-pink-500" : ""
            } cursor-pointer`}
            value="badges"
            onClick={() => dispatch({ component: "lowerNav", label: "badges" })}
          >
            Badges
          </li>
          <li
            className={`${
              lowerNav === "challenges" ? "border-b-2 border-pink-500" : ""
            } cursor-pointer`}
            value="challenges"
            onClick={() =>
              dispatch({ component: "lowerNav", label: "challenges" })
            }
          >
            Challenges
          </li>
        </ul>
        <span className="flex flex-1 text-sm justify-end items-center">
          <time className="pr-32">
            {currentDate.toLocaleString("en-US", dateOptions)}
          </time>
        </span>
      </nav>
      <div className="flex py-4">
        <DailyVibe />         
        <div className="flex-1"></div>
        <div className="border border-black mr-32 ml-6">Macro Charts</div>
      </div>
      <div className="ml-20 mr-32">
        {lowerNav === "journal" ? (
          <DesktopFoodJournal />
        ) : lowerNav === "progress" ? (
          <Progress />
        ) : lowerNav === "badges" ? (
          "Badges"
        ) : lowerNav === "challenges" ? (
          "Challenges"
        ) : lowerNav === "searchResults" ? (
          <FoodSearchResults />
        ) : (
          "Error"
        )}
      </div>
    </Layout>
  );
}

export default withApollo(Dashboard)

Child component

import { useState } from "react";
import { foodDbSearch } from "../../lib/edamam.js";
import { useDashboard } from "../../lib/hooks/useDashboard.js";

export default function FoodSearchBox() {
  const [item, setItem] = useState("");
  const dispatch = useDashboard()[1];

  const handleChange = e => {
    setItem(e.target.value);
  };

  const query = item.replace(" ", "%20"); //  Format the entered food item for the API call

  const handleSubmit = async e => {
    e.preventDefault();
    const list = await foodDbSearch(query); //  Hit the foodDB API
    //  Add the search results to dashboard state 
    dispatch({ component: "foodSearchBox", searchResults: list.hints }); 
    setItem('')  //  Reset input
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        className="w-full border border-purple-400 rounded focus:border-purple-200 px-4 py-2"
        type="text"
        placeholder="Search Food Item"
        name="food"
        value={item}
        onChange={handleChange}
      />
    </form>
  );
}

I'm trying to refactor from what was originally a lot of messy prop drilling with useState hooks being passed around all over the place. I'm using Next.js and I'm trying to stay away from using the Context API or bringing in Redux. I really only need to persist state on individual page components, and it's really just local UI state, as I'm handling most of the data with apollo-hooks.

1

1 Answers

3
votes

The dispatch function that you call in the child component is not the same function as the one in the parent component, and it doesn't update the same state. Different usages of the useDashboard hook return different (state, dispatch) pairs, and they won't affect each other.

If you want the parent component's state to be changeable from the child component, but you don't want to use the context API, you have to pass the parent component's dispatch function (or a callback that uses it) to the child component as a prop.

const Parent = () => {
  const [state, dispatch] = useDashboard();

  return (
    <Child
      updateFoodSearch={(listItems) =>
        dispatch({ component: "foodSearchBox", listItems })
      }
    />
  );
};