How to run effect depending on if a value increases or decreases in React?

This is an expansion on a previous question asking how to run an effect by adding an HTML class once some data changes.

In this case, I’d like to compare the new and old values. If the value increased I could then add an increased class to add some CSS effects temporarily, say 2 seconds. And the same goes for a decrease but with a decreased class.

Answer

Let’s continue off the solution from the previous answer (added below for easier reference).

function useHasRecentlyChanged(variable, timeout = 2000) {
  const firstRender = useRef(true);
  const [hasRecentlyChanged, setHasRecentlyChanged] = useState(false);

  useEffect(() => {
    if (firstRender.current) {
      return;
    }

    setHasRecentlyChanged(true);

    setTimeout(() => {
      setHasRecentlyChanged(false);
    }, timeout);
  }, [variable]);

  useEffect(() => {
    if (firstRender.current) {
      firstRender.current = false;
    }
  });

  return hasRecentlyChanged;
}

It currently returns true if the variable passed to it has changed and then returns false after the timeout time has elapsed. We want to change this function so that it communicates if the value passed to it has increased or decreased recently. The arguments can remain the same but we need to change the interface of the return. We can’t use booleans any more because we now have three possibilities: increased, no change and decreased. I’m going to go with positive integer, zero and negative integer to represent each respectively because we can subtract the current value with the previous value to determine if it has increased or decreased. If the subtraction value is greater than 0 we know the current value has increased, if the subtraction value is less than 0 we know the current value has decreased and if the subtraction value is 0 we know there’s no change.

increased = 1
no change = 0
decreased = -1

We’ll also need to add something to compare the current value with the previous value. We can use the useRef hook to store the value for the next render. There’s even an example of this in the current solution: we store the firstRender state in a ref. Let’s make these changes to the above function:

function useHasRecentlyIncreasedOrDecreased(variable, timeout = 2000) {
  const firstRender = useRef(true);
  const previousValue = useRef();
  const [state, setState] = useState(0);

  useEffect(() => {
    if (firstRender.current) {
      previousValue.current = variable;
      return;
    }

    setState(Math.min(1, Math.max(-1, variable - previousValue.current)))
    
    previousValue.current = variable;

    setTimeout(() => {
      setState(0);
    }, timeout);
  }, [variable]);

  useEffect(() => {
    if (firstRender.current) {
      firstRender.current = false;
    }
  });

  return state;
}

After this, add the effects you want depending on the value returned from this hook. Below is a demo:

const { useEffect, useRef, useState } = React;
const rootElement = document.getElementById('root');

function useHasRecentlyIncreasedOrDecreased(variable, timeout = 2000) {
  const firstRender = useRef(true);
  const previousValue = useRef();
  const [state, setState] = useState(0);

  useEffect(() => {
    if (firstRender.current) {
      previousValue.current = variable;
      return;
    }

    setState(Math.min(1, Math.max(-1, variable - previousValue.current)))
    
    previousValue.current = variable;

    setTimeout(() => {
      setState(0);
    }, timeout);
  }, [variable]);

  useEffect(() => {
    if (firstRender.current) {
      firstRender.current = false;
    }
  });

  return state;
}

function getRandomInt(min, max) {
  const ceilMin = Math.ceil(min);
  const floorMax = Math.floor(max);

  return Math.floor(Math.random() * (floorMax - ceilMin + 1)) + ceilMin;
}

function ListItem({ key, children }) {
  const increasedOrDecreased = useHasRecentlyIncreasedOrDecreased(children);
  
  return (
    <li key={key} className={increasedOrDecreased > 0 ? 'increased' : increasedOrDecreased < 0 ? 'decreased' : undefined}>{children}</li>
  );
}

function App() {
  const [items, setItems] = useState([
    getRandomInt(0, 1000),
    getRandomInt(0, 1000),
    getRandomInt(0, 1000),
    getRandomInt(0, 1000),
    getRandomInt(0, 1000),
    getRandomInt(0, 1000),
    getRandomInt(0, 1000),
    getRandomInt(0, 1000),
    getRandomInt(0, 1000),
  ]);
  
  useEffect(() => {
    const intervalId = setInterval(() => { 
      const newItems = [...items];
      const updateIndex = getRandomInt(0, items.length - 1);
      newItems[updateIndex] = getRandomInt(0, 1000);
      setItems(newItems);
    }, 2000);
    
    return () => clearInterval(intervalId);
  }, [...items]);
 
  return (
    <ul>
      {items.map((item, index) => <ListItem key={index}>{item}</ListItem>)}
    </ul>
  );
}

ReactDOM.render(
  <App />,
  rootElement
);
body {
  font-family: monospace;
  font-size: 20px;
}

li {
  margin: 5px 0;
}

.increased {
  color: green;
}

.increased::after {
  content: " ⬆️"
}

.decreased {
  color: red;
}

.decreased::after {
  content: " ⬇️"
}
<div id="root"></div>
<script crossorigin src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>