rap-2-h
rap-2-h

Reputation: 32058

Waiting for values from unknown children component number before effet

Assuming there is a parent component:

function Parent({ children }) {
  useEffet(() => {
    // Do something when all children 
    // have returned their values via props (or context)
  })
  return <div>{ children }</div>
}

How can I run effect only after all children have done something? I don't know the exact number of children and it can vary:

// In this example there are 4 children
function App() {
  return (
    <Parent>
      <Children />
      <Children />
      <Children />
      <div>
        <Children />
      </div>
    </Parent>
  );
}

The actual problem is that all children can prepare a query (let's say an ElasticSearch Query), and the parent is responsible to actually perform the whole query.

Upvotes: 2

Views: 647

Answers (2)

Jonas Wilms
Jonas Wilms

Reputation: 138537

Well we could wrap the action the Children does into a Promise:

  function usePromises() {
    const promises = useMemo([], []);
    const [result, setResult] = useState(undefined);

    useEffect(() => { // this gets deferred after the initialization of all childrens, therefore all promises were created already
          Promise.all(promises).then(setResult);
     }, []);


    function usePromise() {
      return useMemo(() => {
       let resolve;
       promises.push(new Promise(r => resolve = r));
       return resolve;
      }, []);
    }

   return [result, usePromise];
}

Now create one shared manager (inside of the Parent):

  const [queries, useQueryArrived] = usePromises();
  // queries is either undefined or an array of queries, if all queries arrived this component will rerender
  console.log(queries);

Then pass down useQueryArrived to the children and use it there (inside the Children):

  const queryArrived = useQueryArrived();

  // somewhen:
  queryArrived({ some: "props" });

Now the logic goes as follows:

The Parent gets rendered for the first time, it will create the promises array. The childrens will get initialized and each of them creates a Promise that gets attached to promises the Promise's resolver gets memoized so that the same resolver will be returned on every child rerender. When all children were initialized, React renders and the effect gets fired, which will take all the promises and call Promise.all on them.

Now somewhen queryArrived gets called in the children, the promises resolve, somewhen the last promise resolved, which then call setResult with all the promises results which will cause a rerender on the Parent, that can now use the queries.

Upvotes: 2

Dennis Vash
Dennis Vash

Reputation: 53984

You can introduce a finish state when all children finished "doing something".

  useEffect(() => {
    const totalChildren = React.Children.count(children);
    if (totalChildren === finish) {
      console.log("All Children Done Something");
      setFinished(0);
    }
  }, [finish]);

For example:

function Children({ num, onClick }) {
  return <button onClick={onClick}>{num}</button>;
}
function Parent({ finish, setFinished, children }) {
  useEffect(() => {
    const totalChildren = React.Children.count(children);
    if (totalChildren === finish) {
      console.log("All Children Done Something");
      setFinished(0);
    }
  }, [finish]);
  return <div>{children}</div>;
}

function App() {
  const [num, setNum] = useState(0);
  const [finish, setFinished] = useState(0);

  const onClick = () => {
    setNum(num + 1);
    setFinished(finish + 1);
  };
  return (
    <Parent finish={finish} setFinished={setFinished}>
      <Children num={num} onClick={onClick} />
      <Children num={num + 1} onClick={onClick} />
      <Children num={num + 2} onClick={onClick} />
      <div>
        <Children num={num + 3} onClick={onClick} />
      </div>
    </Parent>
  );
}

Furthermore, you can use React.cloneElement and add "updating finish state" functionality to your children components.

Upvotes: 1

Related Questions