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.