2

Is it appropriate to use useEffect to fetch new state when a prop changes? For example, like this:

const Parent = () => {
  const [parentVal, setParentVal] = useState(0);
  const updateParentVal = () => {
    setParentVal((v) => v + 1);
  };

  return (
    <div style={{ border: "1px solid red", padding: 10 }}>
      <span style={{ marginRight: 10 }}>parentVal: {parentVal}</span>
      <button onClick={updateParentVal}>increment</button>
      <Child parentVal={parentVal} />
    </div>
  );
};
const Child = ({ parentVal }) => {
  const [childVal, setChildVal] = useState(-2);
  const fetchNextChildVal = (currChildVal) => Promise.resolve(currChildVal + 2);

  useEffect(() => {
    (async () => {
      const nextChildVal = await fetchNextChildVal(childVal);

      setChildVal(nextChildVal);
    })();
  }, [parentVal]);

  return (
    <div style={{ border: "1px solid blue", margin: 30, padding: 10 }}>
      <span>childVal: {childVal}</span>
    </div>
  );
};
export default function App() {
  return <Parent />;
}

Even after reading through You Might Not Need an Effect from the React docs, it's not clear to me. Various sections discourage using useEffect to update state, which makes me think that the proper way to handle this situation would be to fetch the data in updateParentVal and pass it down into Child.

In particular, this excerpt from the "Sharing logic between event handlers" section:

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 notification should appear because the user pressed the button, not because the page was displayed! Delete the Effect and put the shared logic into a function called from both event handlers:

I know the section is talking about shared logic between more than one event handler, but it feels to me like it's making a broader point about it being more appropriate to fetch in the event handler.

However, the "Fetching data" section at the end sounds like it's saying that it is appropriate to fetch data in response to a prop changing. So then, I'm not actually sure what the best practice is.

Drew Reese
  • 165,259
  • 14
  • 153
  • 181
Adam Zerner
  • 17,797
  • 15
  • 90
  • 156
  • briefly no because child is already mounted and you can run into many issues (rerendering, infinite loop) you can check this doc for more informations about alternatve solution : https://docs.oracle.com/en/cloud/saas/cx-commerce/21b/dosfa/use-usecallback-and-usememo-efficiently.html#GUID-49549BD0-79C8-45E6-9007-FD26880901B9 – Christa Aug 26 '23 at 04:02
  • @Lapwood Is the issue you're referring to unnecessary re-renders? If so, that isn't a problem in our case, and isn't usually a problem in general. The re-render is usually fast enough where users won't notice. See [this](https://stackoverflow.com/a/9693933/1927876). – Adam Zerner Aug 26 '23 at 04:07
  • unnecessary re-renders for small application it possible isn't a problem but for big project it make defference like re-rendering 1000 childs or more and some users doesnt have a performant device to deal with that – Christa Aug 26 '23 at 04:18

6 Answers6

3

Is it appropriate to use useEffect to fetch new state when a prop changes?

Quite simply, yes, this is an appropriate and correct usage of the useEffect hook. The useEffect hook is used to issue side-effects, e.g. fetching data from an external resource, and its dependency array is used to control when the side-effect may be triggered again during the life of the component, e.g. when a prop value changes. An example scenario is fetching a user's post by their id.

const Component = ({ userId }) => {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    fetch(`/api/posts/${userId}`)
      .then(response => response.json())
      .then(posts => setPosts(posts));
  }, [userId]);

  ...

You do need Effects to synchronize with external systems. For example, you can write an Effect that keeps a jQuery widget synchronized with the React state. You can also fetch data with Effects: for example, you can synchronize the search results with the current search query.

What you might be getting tripped up on in the rest of the "you might not need an effect" section is that it's trying to point out some general React anti-patterns and correct pattern(s) to use.

A few examples:

  • "You don't need Effect to transform data for rendering"

    Just compute the transformed data directly and render. If it's "expensive" then use the useMemo hook to memoize the computed value.

  • "You don't need Effects to handle user events"

    Don't set some state that an event happened so an effect can be issued, just issue the effect from the event handler. Think of a form onSubmit handler.

  • Don't store props into local state and use a useEffect hook to synchronize the state to the prop value, just use the prop value directly.

For your specific code example this isn't an appropriate usage of the useEffect hook. The child component should just directly reference the currChildVal prop value and add 2 to it.

const Child = ({ parentVal }) => {
  return (
    <div style={{ border: "1px solid blue", margin: 30, padding: 10 }}>
      <span>childVal: {parentVal + 2}</span>
    </div>
  );
};
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
2

It's mostly a question of the desired encapsulation of Child. If it is a reusable thing that should be agnostic of why parentVal changed, then yes it might be appropriate.

That tends to be the case if the component is going to be used in unknown situations, such as a lib, or something that's used a lot in your app to the point where you need the decoupling.

But if you do know about how that changes (button click) ahead of time, then yes arguably you can utilise that to remove the effects which would likely to lead to (pretty minor) perf improvements.

adsy
  • 8,531
  • 2
  • 20
  • 31
1

in your case, the Child value should only be fetched and updated if the parent has changed. So, instead of waiting for the parent to change it state then fetch child' data using useEffect, you can instead handle it as an event, I mean fetch the child new value the moment when the parent has trigger the update function not wait until parent is changed.

Here is my solution :

Parent

onst Parent = () => {
  const [parentVal, setParentVal] = useState(0);
  const [childVal, setChildVal] = useState(-2);

  const updateParentVal = async () => {
    setParentVal((v) => v + 1);
    const nextChildVal = await fetchNextChildVal(childVal);
    setChildVal(nextChildVal);
  };
  const fetchNextChildVal = (currChildVal) => Promise.resolve(currChildVal + 2);

  return (
    <div style={{ border: "1px solid red", padding: 10 }}>
      <span style={{ marginRight: 10 }}>parentVal: {parentVal}</span>
      <button onClick={updateParentVal}>increment</button>
      <Child childVal={childVal} />
    </div>
  );
};

Child

const Child = ({ childVal }) => {
  return (
    <div style={{ border: "1px solid blue", margin: 30, padding: 10 }}>
      <span>childVal: {childVal}</span>
    </div>
  );
};

link to sandbox

0

Like @adsy said, I will add that in order to get rid of the useEffect hook, you may use useMemo for a better approach in my opinion. (https://react.dev/reference/react/useMemo)

You can use it similar like this:

const childVal = useMemo(() => parentVal * 2, [parentVal]);

Of course, if you have other functions implemented that need to change the childVal value, like a button or something else within the child component, it is a better approach to use useEffect and use useState for that, but for something simple, that you do not change directly and only changes after some of the props changes, is a good approach.

Galbert
  • 177
  • 1
  • 8
  • I almost suggested this as well but I think he's mostly concerned about asynchronous "fetching stuff" that can't be modelled in this way. Totally correct though and good advice when it's synchronous. – adsy Aug 26 '23 at 16:09
0

A good way to understand best practices is to examine techniques used when fetching data from APIs. I have used these patterns identically across React, Android and iOS.

DATA LOADING

When a view loads it can use code like this to load data, and also to to receive reload events.

useEffect(() => {
    startup();
    return () => cleanup();
}, [id]);

async function startup(): Promise<void> {
    model.eventBus.on(EventNames.ReloadData, onReload);
    await loadData();
}

function cleanup(): void {
   model.eventBus.detach(EventNames.ReloadData, onReload);
}

VIEW MODELS

Views are recreated frequently. but they can use view models that are only created once. The view should be able to quickly bind its state to the view model's data. If a view is recreated the data it uses is immediately available.

When a view model uses IDs its data will swap out as the user navigates, and the view's props will change. Therefore this needs to trigger useEffect to run again, as in my example code above.

VIEW RE-ENTRANCY

As a React developer, also expect re-entrancy. In fact, strict mode in React 18 will call useEffect twice in succession without waiting for any async calls to conplete.

API RESPONSE CACHING

The above requirements lead to a trend for the frontend to cache API responses. So even if the view model's ID and data have changed from ID=2 to ID=4, there will be no fetch if the user navigates back to ID=2.

So calling useEffect many times is common for views that fetch data upon load. You just need to ensure that this does not repeat any expensive work.

SUMMARY

The behavior encouraged in strict mode encourages efficient UIs that serve cached data, but also provide users with a reload option. This also reduces the load on servers. Out of interest, my React demo app uses these techniques, as something to compare against.

Gary Archer
  • 22,534
  • 2
  • 12
  • 24
0

First I would like to summarize the snippet you have given for better understanding.

1.A child component is rendered in the Parent Component.

  1. Parent gets re-rendered whenever the parentVal changes that is on click of the button.

  2. Child component should be re-rendered if parentVal changes. But here is the thing,

As we know in react a component gets re rendered whenever the state or prop changes. In our case on click of the button in parent the state of the parent component first gets updated and starts to re-render which means child will also be re re rendered as new. But this Re rendering is not due to props change in the child but state change in the parents component. Which Implies that we don't need to add the parentVal to depedencies array as the state change in parent itself will trigger the re-render of the child component.

And for your Question,

Is it appropriate to use useEffect to fetch new state when a prop changes?

It is perfectly fine to fetch new data for updating the state and update the state in useEffect during a props change as long as it does not result in a infinte re render.