'Mouse Out of Element and Mouse Into Another Element Does Not Reset State

Code: https://codesandbox.io/s/objective-darwin-w0i5pk?file=/src/App.js

Description: This is just 4 gray squares that each get their own shade of gray. I want to change the background color of each square when the user hovers over each, but I want the hover color to be +10 in RGB of what it was originally.

Issue: When I mouse/hover out of one of the gray squares and mouse/hover into another gray square, the first square does not switch back to its initial color state.

Help: Can someone explain why it is doing this and how to fix it because I have no idea?

Note: I am trying not to use CSS for the hover because I am specifying the backgroundColor with JS.

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

const tabs = [
  { name: "1", img: [] },
  { name: "2", img: [] },
  { name: "3", img: [] },
  { name: "4", img: [] }
];

const initialState = {};

tabs.forEach((t, i) => {
  initialState[i] = false;
});

export default function App() {
  const [hover, setHover] = useState(initialState);

  return (
    <div className="App">
      {tabs.map((t, i) => {
        const v = 50 - (i + 1) * 10;
        const val = hover[i] ? v + 10 : v;

        return (
          <div
            key={t.name}
            className="tab"
            onMouseOver={() => {
              setHover({
                ...hover,
                [i]: true
              });
            }}
            onMouseLeave={() => {
              setHover({
                ...hover,
                [i]: false
              });
            }}
            onMouseOut={() => {
              setHover({
                ...hover,
                [i]: false
              });
            }}
            style={{
              backgroundColor: `rgb(${val}, ${val}, ${val})`,
              height: "100px",
              width: "100px"
            }}
          >
            <p>{t.name}</p>
          </div>
        );
      })}
    </div>
  );
}
.App {
  font-family: sans-serif;
  text-align: center;
  margin: 0;
  padding: 0;
}
* {
  margin: 0;
  padding: 0;
}

This picture only shows the initial state:

not changing back to initial state



Solution 1:[1]

setState calls are not what a human would consider "immediate". Instead, the calls to the state setter as queued inside React internal mechanisms. Consider this:

const [state, setState] = useState(0)

// somewhere

setState(state + 1)
setState(state + 1)

In this case, you do not end up with 2 but 1, because while you call setState twice to increment by one, you really are calling it as:

setState(1)
setState(1)

This is the exact issue in your code with the callbacks, you have

// enter
setState({ ...state, [i]: true })
// leave
setState({ ...state, [i]: false })

so when both get called, you apply the "leave" with the wrong previous state.

This is why setState has another pattern, setState(prevState => nextState)

setState(prevState => prevState + 1)
setState(prevState => prevState + 1)

Like this, you do end up with the value 2 because the second call is then using the "correct" previous state.

In your case, you need:

// enter
setState(prevState => ({ ...prevState, [i]: true }))
// leave
setState(prevState => ({ ...prevState, [i]: false }))

Solution 2:[2]

This is happening because you also keep the previous values in your state. You should update in this way

onMouseOver={() => {
              setHover({
                [i]: true
              });
            }}
            onMouseLeave={() => {
              setHover({
                [i]: false
              });
            }}
            onMouseOut={() => {
              setHover({
                [i]: false
              });
            }}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Jakub Kotrs
Solution 2 Evren