1

I wrote ToggleDisplayMenu first and it works perfectly. I tried writing ToggleMelkesjef and ToggleOrdenselev the exact same way but they do not work. They do get called on their respective button clicks, just the values of isMelkesjef and isOrdenselev do not seem to change.

  //Function to toggle displaymenu
  function ToggleDisplayMenu(name) {
    setStudents(
      students.map((student) =>
        student.name === name
          ? { ...student, isDisplayMenu: !student.isDisplayMenu }
          : student
      )
    );
  }
  //Function to toggle ordenselev
  function ToggleOrdenselev(name) {
    setStudents(
      students.map((student) =>
        student.name === name
          ? { ...student, isOrdenselev: !student.isOrdenselev }
          : student
      )
    );
  }
  //Function to toggle melkesjef
  function ToggleMelkesjef(name) {
    setStudents(
      students.map((student) =>
        student.name === name
          ? { ...student, isMelkesjef: !student.isMelkesjef }
          : student
      )
    );
  }

Full code:

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

let studentnames = [
  {
    name: "Colin",
    birthday: 1.01
  },
  {
    name: "Eskil",
    birthday: 1.01
  },
  {
    name: "Ane",
    birthday: 1.01
  },
  {
    name: "Adrian",
    birthday: 1.01
  },
  {
    name: "Eline",
    birthday: 1.01
  },
  {
    name: "Aurora",
    birthday: 1.01
  },
  {
    name: "Mia",
    birthday: 1.01
  },
  {
    name: "Ella",
    birthday: 1.01
  },
  {
    name: "Magnus",
    birthday: 1.01
  },
  {
    name: "Marin",
    birthday: 1.01
  }
];
studentnames.map(
  (student) => (
    (student.isMelkesjef = false),
    (student.isOrdenselev = false),
    (student.isDisplayMenu = false)
  )
);

function Studentlist(props) {
  const { studentnames } = props;

  const [students, setStudents] = useState(studentnames);

  //Function to toggle displaymenu
  function ToggleDisplayMenu(name) {
    setStudents(
      students.map((student) =>
        student.name === name
          ? { ...student, isDisplayMenu: !student.isDisplayMenu }
          : student
      )
    );
  }
  //Function to toggle ordenselev
  function ToggleOrdenselev(name) {
    setStudents(
      students.map((student) =>
        student.name === name
          ? { ...student, isOrdenselev: !student.isOrdenselev }
          : student
      )
    );
  }
  //Function to toggle melkesjef
  function ToggleMelkesjef(name) {
    setStudents(
      students.map((student) =>
        student.name === name
          ? { ...student, isMelkesjef: !student.isMelkesjef }
          : student
      )
    );
  }
  //Return the actual html element
  return students.map((student) => (
    <div
      className="studentbox"
      id={student.name}
      onClick={() => ToggleDisplayMenu(student.name)}
    >
      {student.isDisplayMenu ? (
        <>
          <button onClick={() => ToggleMelkesjef(student.name)}>
            {student.isMelkesjef ? "Fjern melkesjef" : "Bli melkesjef"}
          </button>
          <button onClick={() => ToggleOrdenselev(student.name)}>
            {student.isOrdenselev ? "Fjern ordenselev" : "Bli Ordenselev"}
          </button>
          <button>{student.isFravær ? "Fjern fravær" : "Bli fravær"}</button>
        </>
      ) : (
        <>
          <h2>{student.name}</h2>
          <h3>{student.birthday}</h3>
        </>
      )}
    </div>
  ));
}

export default function App() {
  return (
    <div className="container">
      <Studentlist studentnames={studentnames}></Studentlist>
    </div>
  );
}

Using console.log and using the ? operator I've determined everything is working except the isMelkesjef and isOrdenselev values do not change, despite the almost identical ToggleDisplayMenu function working just fine.

  • It looks like you have a closure issue over the state values. If you pass a callback to all your setState calls it is resolved. `setStudents((students) => students.map(...));` [sandbox](https://codesandbox.io/s/gallant-feynman-cu0jrk?file=/src/App.js) – pilchard Apr 25 '23 at 21:32
  • But you'll note the amount of duplication. You can combine all your handlers into one and accept the property to toggle as a second parameter. [sandbox](https://codesandbox.io/s/amazing-tereshkova-d9wtip?file=/src/App.js). Also, when using props to set state (a bit of an anti-pattern) you should be sure to implement a useEffect dependent on that prop to reset the state should the prop change. see [How to sync props to state using React hooks : setState()](https://stackoverflow.com/questions/54625831/how-to-sync-props-to-state-using-react-hooks-setstate) – pilchard Apr 25 '23 at 21:40

1 Answers1

2

Solution

You must use an updater function instead of setting state directly when performing multiple updates to the same state in the same render pass.

Instead of:

function ToggleOrdenselev(name) {
  setStudents(
    students.map((student) =>
      ...

Use:

function ToggleOrdenselev(name) {
  setStudents(currentStudents =>
    currentStudents.map((student) =>
      ...

Here is a CodePen with the working code.

A second CodePen with a solution of closing the menu in a separate step. See explanation for more info.


Problem

Consider this:

There are two ways of setting state in a useState hook in React. The first is just passing in the new value:

const [ fruit, setFruit ] = useState('orange')
setFruit(`wild ${fruit}`)
// fruit should be 'wild orange'

This works fine most of the time. React will update the state and queue the component for re-render ASAP (after all ongoing events finish executing).

The second way of updating state is by passing in an updater function that receives the current state value as its argument, and must return an expression describing the next value for the state.

setFruit(currentFruit => `wild ${currentFruit}`)
// fruit should be 'wild orange'

The two forms work identically most of the time, but there are a few exceptions. Most notably calling setState multiple times during the same render pass will not work. Since React can't re-render the component while code is still executing, any call to setState after the first will get a stale value.

const [ fruit, setFruit ] = useState('orange')
setFruit(`wild ${fruit}`)
setFruit(`tender ${fruit}`)
setFruit(`sweet ${fruit}`)
// fruit will be 'sweet orange', not 'sweet tender wild orange'

With an updater function however, the following works:

const [ fruit, setFruit ] = useState('orange')
setFruit(currentFruit => `wild ${currentFruit}`)
setFruit(currentFruit => `tender ${currentFruit}`)
setFruit(currentFruit => `sweet ${currentFruit}`)
// fruit will be 'sweet tender wild orange'

This limitation doesn't affect only multiple updates in the same code block. Any code still executing in the component's event queue must finish before React can re-render the component.

This is what is happening in your code: because events in JS bubble from their target to all parents that may also be listening, you have two handlers executing in the same render pass, both trying to update the value of the same state.

Because of bubbling, the onclick in the <div className="studentbox" /> is executed immediatly after the onclick from any of the buttons that are inside of it, causing the isOrdenselev/isMelkesjef property to be toggled AND causing the isDisplayMenu to be toggled as well, closing the menu in the same click. Two updates to the same state, which can only be done by using updater functions.

If closing the menu in the same click is not the intended behavior, you can avoid bublling just by moving the onclick from the parent div studentbox to somewhere else inside of it, like in the h2 tag:

<h2 onClick={() => ToggleDisplayMenu(student.name)}>{student.name}</h2>

I would still recommend using the updater function to set state, though. It avoids edge cases like this and allows for better optimizations by keeping the function pure.

Second CodePen with this solution of closing the menu in a separate step.

R Simioni
  • 61
  • 4