Why Copilot’s Code Crashed My App (And How I Fixed It)

Rishav Sinha
Published on · 5 min read

The Confusion
I am staring at my frozen browser tab. My laptop fan sounds like it is preparing for takeoff. I just hit "Tab" to accept a code suggestion from GitHub Copilot, and now my entire application is completely unresponsive.
This happens a lot lately. I am building a dashboard in React to show user analytics. Copilot sees me typing a function to fetch data and immediately offers a perfectly formatted block of code. It looks correct. It uses the right variables. I accept it, save the file, and everything breaks.
What confuses me initially is that the AI-generated code looks identical to things I have written a hundred times. It is just a useEffect hook that fetches data when my component loads. But behind the scenes, it is creating a massive infinite loop. The app keeps fetching data, updating the screen, and fetching again, thousands of times a second.
The Plain-English Explanation
Here is a plain-English explanation of why this happens.
Imagine you are following a GPS that tells you to turn left. But every time you turn the steering wheel, the GPS resets. It calculates a brand new route from your exact new position, which again tells you to turn left. You end up doing donuts in the middle of the intersection.
This is what happens when you put an object or an array inside a React dependency array without being careful. A dependency array tells React, "Only run this code again if these specific variables change."
The problem is how JavaScript compares things. If you create a new object, JavaScript sees it as a completely new thing, even if the contents are exactly the same as the old object. Copilot suggested passing a configuration object directly into my dependency array. So, every time the component rendered, it created a "new" object. React saw this "new" object, thought the dependencies changed, and ran the fetch again. Which caused another render. Which created another object. Infinite donuts.
The Smallest Working Example
Here is the smallest working example of how to fix this. The trick is to use a tool called useMemo to tell React to keep the exact same object unless its internal values actually change.
In this code, I am wrapping the configuration object in useMemo so the memory reference stays stable.
// components/UserDashboard.tsx
// Import React, the useEffect hook for side effects, useMemo for caching, and useState for local state
import React, { useEffect, useMemo, useState } from 'react';
// Define the shape of our user data so TypeScript can catch our mistakes
interface UserData {
// The user's unique ID string
id: string;
// The user's display name string
name: string;
}
// Export our main dashboard component so other files can use it
export default function UserDashboard() {
// Create a state variable to hold our user data, starting as null
const [user, setUser] = useState<UserData | null>(null);
// Create a state variable to track if we have a network error, starting as false
const [hasError, setHasError] = useState<boolean>(false);
// We need a config object to pass to our API fetcher
// Instead of recreating this object every render (which Copilot did), we memoize it
const fetchConfig = useMemo(() => {
// Return the actual configuration object we want to use
return {
// Specify the API endpoint we want to hit
endpoint: '/api/users/me',
// Specify the timeout limit in milliseconds
timeout: 5000
};
// Provide an empty dependency array so this object is only created exactly once
}, []);
// Set up our side effect to fetch the data when the component loads
useEffect(() => {
// Define an asynchronous function inside the effect to handle the fetching
const loadData = async () => {
// Start a try block to catch any network errors that might happen
try {
// Make the actual network request using our memoized config object
const response = await fetch(fetchConfig.endpoint);
// Check if the network response is not OK (like a 404 or 500 error)
if (!response.ok) {
// Throw an error to jump straight into the catch block below
throw new Error('Network response failed');
}
// Parse the JSON data from the successful network response
const data: UserData = await response.json();
// Update our React state with the fresh user data
setUser(data);
// Catch any errors that happened during the fetch or parsing process
} catch (error) {
// Update our error state to true so the user knows something went wrong
setHasError(true);
}
};
// Actually call the asynchronous function we just defined above
loadData();
// Pass our memoized fetchConfig into the dependency array
// Because we used useMemo, this reference never changes, preventing the infinite loop!
}, [fetchConfig]);
// If our error state is true, show a fallback message
if (hasError) {
// Return a simple error UI instead of the dashboard
return <div>Something went wrong loading the data.</div>;
}
// If we don't have user data yet, show a loading message
if (!user) {
// Return a simple loading UI while the fetch is happening
return <div>Loading dashboard...</div>;
}
// If everything worked, render the actual dashboard with the user's name
return <div>Welcome back, {user.name}</div>;
}
What to Build Next
Here is a concrete next step you can take today.
Open up your current project and search your codebase for useEffect. Look closely at the array at the very end of those hooks. If you see an object or an array directly inside it, you might be sitting on a ticking time bomb.
Try extracting those objects outside of your component entirely if they don't depend on component state. If they do depend on state, wrap them in useMemo. Copilot is great at typing fast, but it is terrible at understanding how JavaScript handles memory references. You still have to be the pilot.