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 2function useCustomHook() { 3 const [data, setData] = useState(stateData); 4 const [location, setLocation] = useState("state"); 5 6 const handleLocationChange = (selectedLocation) => { 7 if (selectedLocation === "state") { 8 setLocation("state"); 9 setData(stateData); 10 } else if (selectedLocation === "country") { 11 setLocation("country"); 12 setData(countryData); 13 } 14 }; 15 return { data, location, handleLocationChange }; 16} 17 18export default useCustomHook; 19
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 on 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
- Independent hook Instance: Calling
useCustomHook
inLocationFilter
created a separate instance of the hook state for that component. Each component maintains its own state, so state updates in theLocationFilter
component don't affect the state inApp
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
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 reinforced my understanding of custom hooks, the importance of lifting state to a common parent component and best practices for state management in React, to ensure synchronised, consistent UI updates. While custom hooks allow you to share state logic across components, they do not let you share state.
For more detailed information, refer to the React documentation on Custom Hooks and Lifting State Up.