useEffect when all dependencies have changed?

久未见 提交于 2020-12-11 05:59:11

问题


Current useEffect is fired when just one of the dependencies have changed.

How could I update it / use it to fire back when both ( or all ) of the dependencies have changed?


回答1:


You'll need to add some logic to call your effect when all dependencies have changed. Here's useEffectAllDepsChange that should achieve your desired behavior.

The strategy here is to compare the previous deps with the current. If they aren't all different, we keep the previous deps in a ref an don't update it until they are. This allows you to change the deps multiple times before the the effect is called.

import React, { useEffect, useState, useRef } from "react";

// taken from https://usehooks.com/usePrevious/
function usePrevious(value) {
  const ref = useRef();

  useEffect(() => {
    ref.current = value;
  }, [value]);
  
  return ref.current;
}

function useEffectAllDepsChange(fn, deps) {
  const prevDeps = usePrevious(deps);
  const changeTarget = useRef();

  useEffect(() => {
    // nothing to compare to yet
    if (changeTarget.current === undefined) {
      changeTarget.current = prevDeps;
    }

    // we're mounting, so call the callback
    if (changeTarget.current === undefined) {
      return fn();
    }

    // make sure every dependency has changed
    if (changeTarget.current.every((dep, i) => dep !== deps[i])) {
      changeTarget.current = deps;

      return fn();
    }
  }, [fn, prevDeps, deps]);
}

export default function App() {
  const [a, setA] = useState(0);
  const [b, setB] = useState(0);

  useEffectAllDepsChange(() => {
    console.log("running effect", [a, b]);
  }, [a, b]);

  return (
    <div>
      <button onClick={() => setA((prev) => prev + 1)}>A: {a}</button>
      <button onClick={() => setB((prev) => prev + 1)}>B: {b}</button>
    </div>
  );
}

An alternate approach inspired by Richard is cleaner, but with the downside of more renders across updates.

function useEffectAllDepsChange(fn, deps) {
  const [changeTarget, setChangeTarget] = useState(deps);

  useEffect(() => {
    setChangeTarget(prev => {
      if (prev.every((dep, i) => dep !== deps[i])) {
        return deps;
      }

      return prev;
    });
  }, [deps]);

  useEffect(fn, changeTarget);
}



回答2:


I like @AustinBrunkhorst's soultion, but you can do it with less code.

Use a state object that is only updated when your criteria is met, and set it within a 2nd useEffect.

import React, { useEffect, useState } from "react";
import "./styles.css";

export default function App() {
  const [a, setA] = useState(0);
  const [b, setB] = useState(0);
  const [ab, setAB] = useState({a, b});

  useEffect(() => {
    setAB(prev => {
      console.log('prev AB', prev)
      return (a !== prev.a && b !== prev.b) 
        ? {a,b} 
        : prev;  // do nothing
    })
  }, [a, b])

  useEffect(() => {
    console.log('both have changed')
  }, [ab])

  return (
    <div className="App">
      <div>Click on a button to increment its value.</div>
      <button onClick={() => setA((prev) => prev + 1)}>A: {a}</button>
      <button onClick={() => setB((prev) => prev + 1)}>B: {b}</button>
    </div>
  );
}




回答3:


You'll have to track the previous values of your dependencies and check if only one of them changed, or both/all. Basic implementation could look like this:

import React from "react";

const usePrev = value => {
  const ref = React.useRef();

  React.useEffect(() => {
    ref.current = value;
  }, [value]);

  return ref.current;
};

const App = () => {
  const [foo, setFoo] = React.useState(0);
  const [bar, setBar] = React.useState(0);
  const prevFoo = usePrev(foo);
  const prevBar = usePrev(bar);

  React.useEffect(() => {
    if (prevFoo !== foo && prevBar !== bar) {
      console.log("both foo and bar changed!");
    }
  }, [prevFoo, prevBar, foo, bar]);

  return (
    <div className="App">
      <h2>foo: {foo}</h2>
      <h2>bar: {bar}</h2>
      <button onClick={() => setFoo(v => v + 1)}>Increment foo</button>
      <button onClick={() => setBar(v => v + 1)}>Increment bar</button>
      <button
        onClick={() => {
          setFoo(v => v + 1);
          setBar(v => v + 1);
        }}
      >
        Increment both
      </button>
    </div>
  );
};

export default App;

Here is also a CodeSandbox link to play around.

You can check how the usePrev hook works elsewhere, e.g here.




回答4:


To demonstrate how you can compose hooks in various manners, here's my approach. This one doesn't invoke the effect in the initial attribution.

import React, { useEffect, useRef, useState } from "react";
import "./styles.css";

function usePrevious(state) {
  const ref = useRef();

  useEffect(() => {
    ref.current = state;
  });

  return ref.current;
}

function useAllChanged(callback, array) {
  const previousArray = usePrevious(array);

  console.log("useAllChanged", array, previousArray);

  if (previousArray === undefined) return;

  const allChanged = array.every((state, index) => {
    const previous = previousArray[index];
    return previous !== state;
  });

  if (allChanged) {
    callback(array, previousArray);
  }
}

const randomIncrement = () => Math.floor(Math.random() * 4);

export default function App() {
  const [state1, setState1] = useState(0);
  const [state2, setState2] = useState(0);
  const [state3, setState3] = useState(0);

  useAllChanged(
    (state, prev) => {
      alert("Everything changed!");
      console.info(state, prev);
    },
    [state1, state2, state3]
  );

  const onClick = () => {
    console.info("onClick");
    setState1(state => state + randomIncrement());
    setState2(state => state + randomIncrement());
    setState3(state => state + randomIncrement());
  };

  return (
    <div className="App">
      <p>State 1: {state1}</p>
      <p>State 2: {state2}</p>
      <p>State 3: {state3}</p>

      <button onClick={onClick}>Randomly increment</button>
    </div>
  );
}



来源:https://stackoverflow.com/questions/62974717/useeffect-when-all-dependencies-have-changed

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!