cjm2671
cjm2671

Reputation: 19486

How do I set the background color of a number based on whether it goes up or down?

I'm trying to catch a state change for a number. I've got a component that renders a number. When the value changes, I want to set the background to be green or red, depending on whether it moved up or down.

I'm actually not sure this is possible to do in functional component. Am I missing something?

Here's my latest (failed) attempt.

import React, { useState } from 'react'

const FlashNumber = ({ value }) => {
  const [prevValue, setPrevValue] = useState(null)
  const [bgColor, setBgColor] = useState('white')

  React.useEffect(() => {
    if (prevValue !== null) {
      if (value > prevValue) {
        setBgColor('green')
        setTimeout(() => {
          setBgColor('white')
        }, 1000)
      } else if (value < prevValue) {
        setBgColor('red')
        setTimeout(() => {
          setBgColor('white')
        }, 1000)
      }
    }
    setPrevValue(value)
  }, [value, prevValue])

  return <div style={{ backgroundColor: bgColor }}>{value}</div>
}

export default FlashNumber

Upvotes: 0

Views: 85

Answers (2)

Martin
Martin

Reputation: 6162

I would probably try to do most of the work in a css animation and keep the timeout hassle out of the component.

import classnames from "clsx"; // you can replace this with a bit of ternary gymnastics

const FlashNumber = ({ value }) => {
  const prevValue = usePrevious(value);
  return (
    <div className={classnames(
      value > prevValue && 'increasing',
      value < prevValue && 'decreasing',
    )}>
      {value}
    </div>
  );
};
const usePrevious = (value) => {
  const ref = useRef();
  useEffect(() => { ref.current = value; }, [value]);
  return ref.current;
}
@keyframes flash-red {
  0%   { background-color: red; }
  100% { background-color: white; }
}

@keyframes flash-green {
  0%   { background-color: green; }
  100% { background-color: white; }
}

.increasing {
  animation: flash-green 1s linear;
  animation-fill-mode: forwards;
}

.decreasing {
  animation: flash-red 1s linear;
  animation-fill-mode: forwards;
}

Note: this flashes only when the direction is changed. If you want to flash whenever a click happens look into the following section:

const FlashNumber = ({ value }) => {
  const prevValue = usePrevious(value);
  const changed = useAlternating(value, ['flip', 'flop']);

  return (
    <div className={classnames(
      value > prevValue && 'increasing',
      value < prevValue && 'decreasing',
      changed,
    )}>
      {value}
    </div>
  );
};
const useAlternating = (trigger, range) => {
  const [prev, setPrev] = useState(trigger);
  const [index, setIndex] = useState(0);
  if (trigger !== prev) {
    setPrev(trigger);
    setIndex(i => (i + 1) % range.length);
  }
  return range[index];
}
@keyframes flash-flip {
  0%   { background-color: var(--color); }
  100% { background-color: white; }
}

@keyframes flash-flop {
  0%   { background-color: var(--color); }
  100% { background-color: white; }
}

.increasing { --color: green; }

.decreasing { --color: red; }

.flip {
  animation: flash-flip 1s linear;
  animation-fill-mode: forwards;
}

.flop {
  animation: flash-flop 1s linear;
  animation-fill-mode: forwards;
}

Upvotes: 0

Sergey Sosunov
Sergey Sosunov

Reputation: 4600

useEffect can return a cleanup function, in that way you will be able to stop "previously set" timeout callbacks from execution.

Codesandbox: https://codesandbox.io/s/cool-dream-inkw4u?file=/src/App.js

const { useMemo, useState, useRef, useEffect } = React;

function App() {
  const [val, setVal] = useState(0);
  return (
    <div className="App">
      <button onClick={() => setVal((x) => x + 1)}>+</button>
      <button onClick={() => setVal((x) => x - 1)}>-</button>
      <FlashNumber value={val} />
    </div>
  );
}

const FlashNumber = ({ value }) => {
  const prevValueRef = useRef(value);
  const [bgColor, setBgColor] = useState("white");

  useEffect(() => {
    if (value > prevValueRef.current) {
      setBgColor("green");
    } else if (value < prevValueRef.current) {
      setBgColor("red");
    }
    prevValueRef.current = value;
    const timeout = setTimeout(() => {
      setBgColor("white");
    }, 1000);
    return () => clearTimeout(timeout);
  }, [value]);

  return <div style={{ backgroundColor: bgColor }}>{value}</div>;
};

// v18.x+
ReactDOM.createRoot(
    document.getElementById("root")
).render(
    <App />
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>

<div id="root"></div>

Upvotes: 1

Related Questions