Fixing UI Update Issues with Custom Hooks in React

Published on Monday, 27 November 2024

Identifying UI Update Problem

If you’ve been working with state management in React, you’ve likely encountered this frustrating scenario: your state updates correctly ( as seen in the React DevTools or console logs), but your UI doesn’t reflect those changes.

I recently encountered this challenge while working with a React custom hook, and I'd like to share how I resolved it.

The code snippet below shows the project structure I was working with.

1// useCustomHook.jsx
2
3function useCustomHook() {
4    const [data, setData] = useState(stateData);
5    const [location, setLocation] = useState("state");
6
7    const handleLocationChange = (selectedLocation) => {
8        if (selectedLocation === "state") {
9            setLocation("state");
10            setData(stateData);
11        } else if (selectedLocation === "country") {
12            setLocation("country");
13            setData(countryData);
14        }
15    };
16    return {
17        data,
18        location,
19        handleLocationChange
20    };
21}
22
23export default useCustomHook;
24
1// App.jsx
2
3import useCustomHook from "./useCustomHook";
4import LocationFilter from "./LocationFilter";
5import Table from "./Table";
6import "./App.css";
7
8export function App() {
9  const { data } = useCustomHook();
10
11  return (
12    <div className="App">
13      <LocationFilter />
14      <Table data={data} />
15    </div>
16  );
17}
18
19export default App;
20
1// LocationFilter.jsx
2
3import useCustomHook from "./useCustomHook";
4
5function LocationFilter() {
6  const { location, handleLocationChange } = useCustomHook();
7
8  const handleFilterByState = () => {
9    handleLocationChange("state");
10  };
11
12  const handleFilterByCountry = () => {
13    handleLocationChange("country");
14  };
15
16  return (
17    <div>
18      <button onClick={handleFilterByState}>View State Data</button>
19      <button onClick={handleFilterByCountry}>View Country Data</button>
20    </div>
21  );
22}
23
24export default LocationFilter;
25

I created a custom hook to manage state changes when users click the button to view either state-specific or country-wide data.

In App.jsx, which serves as the parent component, the custom hook is called to access the data state and passes it down to the Table component (child) as props.

In LocationFilter component, I initially instantiated the custom hook directly to access the location and handleLocationChange state, which was intended to manage the data displayed in the table according to a “state” or “location” filter.”

However, when I click the view state data or view country data button, there is no change in the UI.

I checked the location state inside the LocationFilter component using the React Developer Tools, and I saw that the location and data states were updating as expected with the correct value. However, the UI did not reflect the changes to the data state when I clicked the button.

What Caused My React State Management Issues?

💡 My initial approach seemed logical: I assumed that calling the custom hook in each component would allow each to access and share the same state instance.

Flowchart: Before Lifting State Up

flowchart-diagram-before-state

  • Independent hook Instance: Calling useCustomHook in LocationFilter created a separate instance of the hook state for that component. Each component maintains its own state, so state updates in the LocationFilter component don't affect the state in App component.
  • State synchronisation issue: Clicking the button does not update the App component's state, leading to a mismatch between the parent and child components. As the parent's state remains unchanged, the UI doesn't reflect updates from the child, preventing re-renders in both components.

The Solution: Lifting State Up

React does not synchronise state between components unless you pass it down via props or context. To resolve this, I removed the state hook call from LocationFilter component, fetched the state needed in the App component, and passed it down as props to the LocationFilter component. This ensures both components share the same state, changes in the App, syncing both components data.

Flowchart: After Lifting State Up

flowchart-diagram-after-state

Here’s the updated code for LocationFilter and App

1// LocationFilter.jsx
2
3function LocationFilter({ location, handleLocationChange }) {
4  const handleFilterByState = () => {
5    handleLocationChange("state");
6  };
7
8  const handleFilterByCountry = () => {
9    handleLocationChange("country");
10  };
11
12  return (
13    <div>
14      <button onClick={handleFilterByState}>View State Data</button>
15      <button onClick={handleFilterByCountry}>View Country Data</button>
16    </div>
17  );
18}
19
20export default LocationFilter;
21
1// App.jsx
2
3import useCustomHook from "./useCustomHook";
4import LocationFilter from "./LocationFilter";
5import Table from "./Table";
6import "./App.css";
7
8export function App() {
9  const { data, location, handleLocationChange } = useCustomHook();
10
11  return (
12    <div className="App">
13      <LocationFilter
14        location={location}
15        handleLocationChange={handleLocationChange}
16      />
17      <Table data={data} />
18    </div>
19  );
20}
21
22export default App;
23

Conclusion

This experience has deepened my understanding of custom hooks and highlighted the importance of strategic state placement in React. While custom hooks allow you to share logic across components, they do not let you share state.

When managing state, prioritize keeping it in the component that primarily owns and uses it. If other components depend on updates from that state, consider lifting only the necessary data to a shared parent to ensure synchronized and consistent UI updates, without causing unnecessary re-renders.

For more detailed information, refer to the React documentation on Custom Hooks and Lifting State Up.

Articles written by Kelechi Ogbonna. All rights reserved

Built with NextJs Typescript and TailwindCSS