*Not all React performance issues show up as errors.
*
Some appear quietly during development, when a simple UI interaction feels heavier than it should.
This article walks through a real dev-time issue caused by prop drilling and poor state placement — and how restructuring state fixed unnecessary re-renders, API calls, and heavy UI updates.
This was a React application where a parent component handled:
- Fetching data from an API
- Rendering a Highcharts-based visualization (expensive to re-render)
Deep in the component tree (2–3 levels down), there was a child component with a button.
Requirement:
When the button is clicked, show a modal.
Simple on paper.
The Initial Implementation
The modal state was defined in the parent component.
const Parent = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
useEffect(() => {
fetchData();
}, []);
return <Child setIsModalOpen={setIsModalOpen} />;
};
The setIsModalOpen function was passed down multiple levels via props until it reached the button.
<button onClick={() => setIsModalOpen(true)}>
Open modal
</button>
Functionally, this worked.
Architecturally, it was problematic.
What I Observed During Testing
While casually testing the feature in development, I noticed:
- A visible UI pause on button click
- Highcharts re-rendering
- API calls being triggered again
Every click caused a parent state update, which meant:
- Parent component re-rendered
- Side effects tied to the parent executed again
- Expensive UI re-initialized unnecessarily
Nothing was broken — but the cost was real.
The Root Cause
The issue was state placement.
- The modal state was UI-only.
- It lived in a parent component responsible for data fetching and heavy rendering.
- Prop drilling tightly coupled a local interaction to global side effects
- This is a common architectural leak in React applications as they grow.
The Fix: Isolate UI State with Context
The solution was to remove modal state from the parent entirely and introduce a dedicated Context for modal visibility.
const ModalContext = createContext(null);
export const ModalProvider = ({ children }) => {
const [isOpen, setIsOpen] = useState(false);
return (
<ModalContext.Provider value={{ isOpen, setIsOpen }}>
{children}
</ModalContext.Provider>
);
};
The provider was placed at the top-most boundary where modal state made sense.
Components that needed to open or close the modal consumed the context directly.
const { setIsOpen } = useContext(ModalContext);
No prop drilling. No parent involvement.
The Result
After this change:
- Parent components no longer re-rendered on modal open
- API calls executed only when explicitly intended
- Highcharts remained stable
- UI interactions felt immediate and predictable
The behavior was the same for users, but the architecture was cleaner and more performant.
Key Takeaways
- Prop drilling can hide performance issues, even when code is correct
- UI-only state should not live alongside data-fetching logic
- If a button click triggers unrelated side effects, state boundaries are wrong
- Context API is not just for global data — it is effective for isolating UI concerns
*Performance issues in React are often design issues in disguise.
*
Top comments (0)