You might not need an effect Flashcards

1
Q

You might not need an effect

A

Effects are an escape hatch from the React paradigm. They let you “step outside” of React and synchronize your components with some external system like a non-React widget, network, or the browser DOM. If there is no external system involved (for example, if you want to update a component’s state when some props or state change), you shouldn’t need an Effect. Removing unnecessary Effects will make your code easier to follow, faster to run, and less error-prone.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
2
Q

How to remove unnecessary effects

A
  • You don’t need Effects to transform data for rendering.

For example, let’s say you want to filter a list before displaying it. You might feel tempted to write an Effect that updates a state variable when the list changes.

However, this is inefficient. When you update your component’s state, React will first call your component functions to calculate what should be on the screen.

Then React will “commit” these changes to the DOM, updating the screen. Then React will run your Effects. If your Effect also immediately updates the state, this restarts the whole process from scratch! To avoid the unnecessary render passes, transform all the data at the top level of your components.

That code will automatically re-run whenever your props or state change.

  • You don’t need Effects to handle user events.

For example, let’s say you want to send an /api/buy POST request and show a notification when the user buys a product. In the Buy button click event handler, you know exactly what happened.

By the time an Effect runs, you don’t know what the user did (for example, which button was clicked). This is why you’ll usually handle user events in the corresponding event handlers.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
3
Q

Updating state based on props or state

A

When something can be calculated from the existing props or state, don’t put it in state. Instead, calculate it during rendering. This makes your code faster (you avoid the extra “cascading” updates), simpler (you remove some code), and less error-prone (you avoid bugs caused by different state variables getting out of sync with each other).

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
4
Q

Caching expensive calculations

A

You can cache (or “memoize”) an expensive calculation by wrapping it in a useMemo Hook:

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
  const visibleTodos = useMemo(() => {
    // ✅ Does not re-run unless todos or filter change
    return getFilteredTodos(todos, filter);
  }, [todos, filter]);
  // ...
}

This tells React that you don’t want the inner function to re-run unless either todos or filter have changed. React will remember the return value of getFilteredTodos() during the initial render.

During the next renders, it will check if todos or filter are different. If they’re the same as last time, useMemo will return the last result it has stored. But if they are different, React will call the wrapped function again (and store that result instead).

The function you wrap in useMemo runs during rendering, so this only works for pure calculations.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
5
Q

How to tell if a calculation is expensive?

A

In general, unless you’re creating or looping over thousands of objects, it’s probably not expensive. If you want to get more confidence, you can add a console log to measure the time spent in a piece of code:

console.time(‘filter array’);
const visibleTodos = getFilteredTodos(todos, filter);
console.timeEnd(‘filter array’);

Perform the interaction you’re measuring (for example, typing into the input). You will then see logs like filter array: 0.15ms in your console.

If the overall logged time adds up to a significant amount (say, 1ms or more), it might make sense to memoize that calculation. As an experiment, you can then wrap the calculation in useMemo to verify whether the total logged time has decreased for that interaction or not:

console.time('filter array');
const visibleTodos = useMemo(() => {
  return getFilteredTodos(todos, filter); // Skipped if todos and filter haven't changed
}, [todos, filter]);
console.timeEnd('filter array');

useMemo won’t make the first render faster. It only helps you skip unnecessary work on updates.

Keep in mind that your machine is probably faster than your users’ so it’s a good idea to test the performance with an artificial slowdown. For example, Chrome offers a CPU Throttling option for this.

Also note that measuring performance in development will not give you the most accurate results. (For example, when Strict Mode is on, you will see each component render twice rather than once.) To get the most accurate timings, build your app for production and test it on a device like your users have.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
6
Q

Resetting all state when a prop changes

A

This ProfilePage component receives a userId prop. The page contains a comment input, and you use a comment state variable to hold its value.

One day, you notice a problem: when you navigate from one profile to another, the comment state does not get reset.

As a result, it’s easy to accidentally post a comment on a wrong user’s profile. To fix the issue, you want to clear out the comment state variable whenever the userId changes:

export default function ProfilePage({ userId }) {
const [comment, setComment] = useState(‘’);

  // 🔴 Avoid: Resetting state on prop change in an Effect
  useEffect(() => {
    setComment('');
  }, [userId]);
  // ...
}

This is inefficient because ProfilePage and its children will first render with the stale value, and then render again. It is also complicated because you’d need to do this in every component that has some state inside ProfilePage.

Instead, you can tell React that each user’s profile is conceptually a different profile by giving it an explicit key. Split your component in two and pass a key attribute from the outer component to the inner one:

export default function ProfilePage({ userId }) {
return (

);
}

function Profile({ userId }) {
  // ✅ This and any other state below will reset on key change automatically
  const [comment, setComment] = useState('');
  // ...
}

Normally, React preserves the state when the same component is rendered in the same spot. By passing userId as a key to the Profile component, you’re asking React to treat two Profile components with different userId as two different components that should not share any state.

Whenever the key (which you’ve set to userId) changes, React will recreate the DOM and reset the state of the Profile component and all of its children. As a result, the comment field will clear out automatically when navigating between profiles.

Note that in this example, only the outer ProfilePage component is exported and visible to other files in the project. Components rendering ProfilePage don’t need to pass the key to it: they pass userId as a regular prop. The fact ProfilePage passes it as a key to the inner Profile component is an implementation detail.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
7
Q

Adjusting some state when a prop changes

A

Sometimes, you might want to reset or adjust a part of the state on a prop change, but not all of it.

This List component receives a list of items as a prop, and maintains the selected item in the selection state variable. You want to reset the selection to null whenever the items prop receives a different array:

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selection, setSelection] = useState(null);
  // 🔴 Avoid: Adjusting state on prop change in an Effect
  useEffect(() => {
    setSelection(null);
  }, [items]);
  // ...
}

This, too, is not ideal. Every time the items change, the List and its child components will render with a stale selection value at first. Then React will update the DOM and run the Effects. Finally, the setSelection(null) call will cause another re-render of the List and its child components, restarting this whole process again.

Start by deleting the Effect. Instead, adjust the state directly during rendering:

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selection, setSelection] = useState(null);
  // Better: Adjust the state while rendering
  const [prevItems, setPrevItems] = useState(items);
  if (items !== prevItems) {
    setPrevItems(items);
    setSelection(null);
  }
  // ...
}

This pattern can be hard to understand, but it’s better than updating state in an Effect. In the above example, setSelection is called directly during a render. React will re-render the List immediately after it exits with a return statement. By that point, React hasn’t rendered the List children or updated the DOM yet, so this lets the List children skip rendering the stale selection value.

Before moving on, consider whether you can further simplify the requirements to calculate everything during rendering. For example, instead of storing (and resetting) the selected item, you can store the selected item ID:

function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selectedId, setSelectedId] = useState(null);
  // ✅ Best: Calculate everything during rendering
  const selection = items.find(item => item.id === selectedId) ?? null;
  // ...
}

Now there is no need to “adjust” the state at all. If the item with the selected ID is in the list, it remains selected. If it’s not, the selection calculated during rendering will be null because no matching item was found. This behavior is a bit different, but arguably it’s better because most changes to items now preserve the selection. However, you’d need to use selection in all the logic below because an item with selectedId might not exist.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
8
Q

Sharing logic between event handlers

A

Let’s say you have a product page with two buttons (Buy and Checkout) that both let you buy that product. You want to show a notification toast whenever the user puts the product in the cart. Adding the showToast() call to both buttons’ click handlers feels repetitive so you might be tempted to place this logic in an Effect.

This Effect is unnecessary. It will also most likely cause bugs. For example, let’s say that your app “remembers” the shopping cart between the page reloads. If you add a product to the cart once and refresh the page, the notification toast will appear again. It will keep appearing every time you refresh that product’s page. This is because product.isInCart will already be true on the page load, so the Effect above will call showToast().

When you’re not sure whether some code should be in an Effect or in an event handler, ask yourself why this code needs to run. Use Effects only for code that should run because the component was displayed to the user. In this example, the toast should appear because the user pressed the button, not because the product page was displayed!

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
9
Q

Sending a POST request

A

This Form component sends two kinds of POST requests. It sends an analytics event when it mounts. When you fill in the form and click the Submit button, it will send a POST request to the /api/register endpoint:

function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  // ✅ Good: This logic should run because the component was displayed
  useEffect(() => {
    post('/analytics/event', { eventName: 'visit_form' });
  }, []);
  // 🔴 Avoid: Event-specific logic inside an Effect
  const [jsonToSubmit, setJsonToSubmit] = useState(null);
  useEffect(() => {
    if (jsonToSubmit !== null) {
      post('/api/register', jsonToSubmit);
    }
  }, [jsonToSubmit]);
  function handleSubmit(e) {
    e.preventDefault();
    setJsonToSubmit({ firstName, lastName });
  }
  // ...
}

The analytics POST request should remain in an Effect. This is because the reason to send the analytics event is that the form was displayed.

However, the /api/register POST request is not caused by the form being displayed. You only want to send the request at one specific moment in time: when the user presses the button. It should only ever happen on that particular interaction.

function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  // ✅ Good: This logic runs because the component was displayed
  useEffect(() => {
    post('/analytics/event', { eventName: 'visit_form' });
  }, []);
  function handleSubmit(e) {
    e.preventDefault();
    // ✅ Good: Event-specific logic is in the event handler
    post('/api/register', { firstName, lastName });
  }
  // ...
}

When you choose whether to put some logic into an event handler or an Effect, the main question you need to answer is what kind of logic it is from the user’s perspective. If this logic is caused by a particular interaction, keep it in the event handler. If it’s caused by the user seeing the component on the screen, keep it in the Effect.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
10
Q

Initializing the application

A

Some logic should only run once when the app loads. You might place it in an Effect in the top-level component:

function App() {
  // 🔴 Avoid: Effects with logic that should only ever run once
  useEffect(() => {
    loadDataFromLocalStorage();
    checkAuthToken();
  }, []);
  // ...
}

However, you’ll quickly discover that it runs twice in development. This can cause issues—for example, maybe it invalidates the authentication token because the function wasn’t designed to be called twice. In general, your components should be resilient to being remounted.

This includes your top-level App component. Although it may not ever get remounted in practice in production, following the same constraints in all components makes it easier to move and reuse code.

If some logic must run once per app load rather than once per component mount, you can add a top-level variable to track whether it has already executed, and always skip re-running it:

let didInit = false;

function App() {
  useEffect(() => {
    if (!didInit) {
      didInit = true;
      // ✅ Only runs once per app load
      loadDataFromLocalStorage();
      checkAuthToken();
    }
  }, []);
  // ...
}

You can also run it during module initialization and before the app renders:

if (typeof window !== 'undefined') { // Check if we're running in the browser.
   // ✅ Only runs once per app load
  checkAuthToken();
  loadDataFromLocalStorage();
}
function App() {
  // ...
}

Code at the top level runs once when your component is imported—even if it doesn’t end up being rendered. To avoid slowdown or surprising behavior when importing arbitrary components, don’t overuse this pattern. Keep app-wide initialization logic to root component modules like App.js or in your application’s entry point module.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
11
Q

Notifying parent components about state changes

A

Let’s say you’re writing a Toggle component with an internal isOn state which can be either true or false.

There are a few different ways to toggle it (by clicking or dragging). You want to notify the parent component whenever the Toggle internal state changes, so you expose an onChange event and call it from an Effect:

function Toggle({ onChange }) {
  const [isOn, setIsOn] = useState(false);
  // 🔴 Avoid: The onChange handler runs too late
  useEffect(() => {
    onChange(isOn);
  }, [isOn, onChange])
  function handleClick() {
    setIsOn(!isOn);
  }
  function handleDragEnd(e) {
    if (isCloserToRightEdge(e)) {
      setIsOn(true);
    } else {
      setIsOn(false);
    }
  }
  // ...
}

Like earlier, this is not ideal. The Toggle updates its state first, and React updates the screen. Then React runs the Effect, which calls the onChange function passed from a parent component. Now the parent component will update its own state, starting another render pass. It would be better to do everything in a single pass instead.

function Toggle({ onChange }) {
  const [isOn, setIsOn] = useState(false);
  function updateToggle(nextIsOn) {
    // ✅ Good: Perform all updates during the event that caused them
    setIsOn(nextIsOn);
    onChange(nextIsOn);
  }
  function handleClick() {
    updateToggle(!isOn);
  }
  function handleDragEnd(e) {
    if (isCloserToRightEdge(e)) {
      updateToggle(true);
    } else {
      updateToggle(false);
    }
  }
  // ...
}

With this approach, both the Toggle component and its parent component update their state during the event. React batches updates from different components together, so there will only be one render pass as a result.

You might also be able to remove the state altogether, and instead receive isOn from the parent component:

// ✅ Also good: the component is fully controlled by its parent
function Toggle({ isOn, onChange }) {
  function handleClick() {
    onChange(!isOn);
  }
  function handleDragEnd(e) {
    if (isCloserToRightEdge(e)) {
      onChange(true);
    } else {
      onChange(false);
    }
  }
  // ...
}

“Lifting state up” lets the parent component fully control the Toggle by toggling the parent’s own state. This means the parent component will have to contain more logic, but there will be less state overall to worry about. Whenever you try to keep two different state variables synchronized, it’s a sign to try lifting state up instead!

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
12
Q

Passing data to the parent

A

This Child component fetches some data and then passes it to the Parent component in an Effect:

function Parent() {
  const [data, setData] = useState(null);
  // ...
  return ;
}
function Child({ onFetched }) {
  const data = useSomeAPI();
  // 🔴 Avoid: Passing data to the parent in an Effect
  useEffect(() => {
    if (data) {
      onFetched(data);
    }
  }, [onFetched, data]);
  // ...
}

In React, data flows from the parent components to their children. When you see something wrong on the screen, you can trace where the information comes from by going up the component chain until you find which component passes the wrong prop or has the wrong state.

When child components update the state of their parent components in Effects, the data flow becomes very difficult to trace. Since both the child and the parent component need the same data, let the parent component fetch that data, and pass it down to the child instead:

function Parent() {
  const data = useSomeAPI();
  // ...
  // ✅ Good: Passing data down to the child
  return ;
}
function Child({ data }) {
  // ...
}

This is simpler and keeps the data flow predictable: the data flows down from the parent to the child.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
13
Q

Fetching data

A

Many apps use Effects to kick off data fetching. It is quite common to write a data fetching Effect like this:

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [page, setPage] = useState(1);
  useEffect(() => {
    // 🔴 Avoid: Fetching without cleanup logic
    fetchResults(query, page).then(json => {
      setResults(json);
    });
  }, [query, page]);
  function handleNextPageClick() {
    setPage(page + 1);
  }
  // ...
}

You don’t need to move this fetch to an event handler.

This might seem like a contradiction with the earlier examples where you needed to put the logic into the event handlers! However, consider that it’s not the typing event that’s the main reason to fetch. Search inputs are often prepopulated from the URL, and the user might navigate Back and Forward without touching the input.

It doesn’t matter where page and query come from. While this component is visible, you want to keep results synchronized with data from the network according to the current page and query. This is why it’s an Effect.

However, the code above has a bug. Imagine you type “hello” fast. Then the query will change from “h”, to “he”, “hel”, “hell”, and “hello”. This will kick off separate fetches, but there is no guarantee about which order the responses will arrive in.

For example, the “hell” response may arrive after the “hello” response. Since it will call setResults() last, you will be displaying the wrong search results. This is called a “race condition”: two different requests “raced” against each other and came in a different order than you expected.

To fix the race condition, you need to add a cleanup function to ignore stale responses:

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [page, setPage] = useState(1); 
  useEffect(() => {
    let ignore = false;
    fetchResults(query, page).then(json => {
      if (!ignore) {
        setResults(json);
      }
    });
    return () => {
      ignore = true;
    };
  }, [query, page]);
  function handleNextPageClick() {
    setPage(page + 1);
  }
  // ...
}

This ensures that when your Effect fetches data, all responses except the last requested one will be ignored.

Handling race conditions is not the only difficulty with implementing data fetching. You might also want to think about how to cache the responses (so that the user can click Back and see the previous screen instantly instead of a spinner), how to fetch them on the server (so that the initial server-rendered HTML contains the fetched content instead of a spinner), and how to avoid network waterfalls (so that a child component that needs to fetch data doesn’t have to wait for every parent above it to finish fetching their data before it can start). These issues apply to any UI library, not just React. Solving them is not trivial, which is why modern frameworks provide more efficient built-in data fetching mechanisms than writing Effects directly in your components.

If you don’t use a framework (and don’t want to build your own) but would like to make data fetching from Effects more ergonomic, consider extracting your fetching logic into a custom Hook like in this example:

function SearchResults({ query }) {
  const [page, setPage] = useState(1); 
  const params = new URLSearchParams({ query, page });
  const results = useData(`/api/search?${params}`);
  function handleNextPageClick() {
    setPage(page + 1);
  }
  // ...
}
function useData(url) {
  const [result, setResult] = useState(null);
  useEffect(() => {
    let ignore = false;
    fetch(url)
      .then(response => response.json())
      .then(json => {
        if (!ignore) {
          setResult(json);
        }
      });
    return () => {
      ignore = true;
    };
  }, [url]);
  return result;
}

You’ll likely also want to add some logic for error handling and to track whether the content is loading. You can build a Hook like this yourself or use one of the many solutions already available in the React ecosystem. Although this alone won’t be as efficient as using a framework’s built-in data fetching mechanism, moving the data fetching logic into a custom Hook will make it easier to adopt an efficient data fetching strategy later.

In general, whenever you have to resort to writing Effects, keep an eye out for when you can extract a piece of functionality into a custom Hook with a more declarative and purpose-built API like useData above. The fewer raw useEffect calls you have in your components, the easier you will find to maintain your application.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
14
Q

Summary

A
  • If you can calculate something during render, you don’t need an Effect.
  • To cache expensive calculations, add useMemo instead of useEffect.
  • To reset the state of an entire component tree, pass a different key to it.
  • To reset a particular bit of state in response to a prop change, set it during rendering.
  • Code that needs to run because a component was displayed should be in Effects, the rest should be in events.
  • If you need to update the state of several components, it’s better to do it during a single event.
  • Whenever you try to synchronize state variables in different components, consider lifting state up.
  • You can fetch data with Effects, but you need to implement cleanup to avoid race conditions.
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
15
Q

Common examples of when you may not need to use effects

A
  • Updating state based on props or state
  • Caching expensive calculations
  • Resetting all state when a prop changes
  • Adjusting some state when a prop changes
  • Sharing logic between event handlers
  • Sending a POST request
  • Initializing the application
  • Notifying parent components about state changes
  • Passing data to the parent
  • Subscribing to an external store
  • Fetching data
How well did you know this?
1
Not at all
2
3
4
5
Perfectly