简体   繁体   中英

React component with two dependent useEffect hooks

What follows is my first attempt at the component, which is supposed to re-query data when the filters prop changes. Additionaly, whenever such a change is detected it should wrap back to the first page (first occurence of useEffect ). Thirdly, the user should be able to go to the next page manually ( nextPage callback).

function Fetcher(filters) {
  const [page, setPage] = React.useState(0);

  const nextPage = React.useCallback(() => {
    setPage((p) => p + 1);
  }, []);

  React.useEffect(() => {
    setPage(0);
  }, [filters]);

  React.useEffect(() => {
    externalRequest(filters, page);
  }, [filters, page]);

  return <button onClick={nextPage}></button>;
}

Unfortunately this will naturally won't work properly, as resetting the page in the first effect hook will be performed asynchronously and picked up belatedly: if the user sits on page 1 and filters change, externalRequest will be fired twice in the following manner:

  1. externalRequest(1, newFilters)
  2. externalRequest(0, newFilters)

What I'd consider to be a workaround is either to store the page as a mutable reference as follows:

function Fetcher(filters) {
  const page = React.useRef(0);

  const nextPage = React.useCallback(() => {
    page.current = page.current + 1;
  }, []);

  React.useEffect(() => {
    page.current = 0;
  }, [filters]);

  React.useEffect(() => {
    externalRequest(filters, page);
  }, [filters, page.current]);

  return <button onClick={nextPage}></button>;
}

or lift it up and pass from the outside as the prop (which is not ideal, as I want to avoid leaking this dependency outside). The issue with the former solution (using useRef ) is that the component won't re-render when nextPage is called and the re-cycle would need to be forced. The problem with the latter is that it puts the 'burden' of managing the page dependency outside. It solves it though. My question is whether there exists a way to structure the Fetcher so that we get the best of both worlds ie page state internally in the component and it remaining part of the state, rather than a reference? I'm sorry if this example seems a bit contrived, but I feel this pattern occurs quite often and I'd be really grateful for some input on the matter!

Thank you Anees, that was so simple.. There's one edge caveat that's not addressed by your solution and it's the case in which filters change, while still on page 0 - in this case page dependent effect won't fire. I've ultimately opted in for the following:

function Fetcher(props) {
  const [page, setPage] = useState(() => () => 0);

  const nextPage = useCallback(() => {
    setPage((p) => () => p() + 1);
  }, []);

  // do not run on the mount, but only when props change
  // as otherwise the initial request is fired twice 
  useUpdate(() => {
    setPage(() => () => 0);
  }, [props.filters]);

  useEffect(() => {
    const actualPage = page();
  }, [page]);

  return <button onClick={nextPage}>Next Page</button>;
}

Laziness of the state is irrelevant in this case - I just "wrap it" in the function so that two page instances are referentially different, even if they return the same number. Also, page resetter doesn't run upon mount so that we don't query twice.

You can resolve this issue by removing the filters from the second effect dependency

Why? since you are making sure the page will trigger re-render for effect, so, by setting the filter in both effects, that means trigger twice, one for filter and then page also updates so this second render will be trigger.

You can check the demo here .

function Fetcher(props) {
  const [page, setPage] = useState(0);

  const nextPage = useCallback(() => {
    setPage((p) => p + 1);
  }, []);

  useEffect(() => {
    setPage(0);
  }, [props.filters]);

  useEffect(() => {
    console.log(props.filters, page);
  }, [page]);

  return <button onClick={nextPage}>Next Page</button>;
}

There's an important note: each dependency will trigger re-render when its update, so that we put it inside the dependency array, but you need to make sure about the rendering tree too.

Also, you have another solution, by saving the old filter and checking the new value if it's equal old one or not.

Also, you can use the re-structure component and build function and call what you needed based on conditions...

And I don't recommend to useRef in this case... it's not a solution for a normal state, and I think the first solution is fair.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM