skip to Main Content

I’m trying to create a simple application usign React and Leva js.

Basically there is an array of items and each item has a name and an array of numbers. The Leva panel contains two selects and the user can selects two items from the array of items.

If the two selected items have the same lenght, it’s ok, otherwise the app should returns an error.

Here the main code and here a working demo.

App.jsx:

export const App = () => {
  const [haveSameNumberOfValues, setHaveSameNumberOfValues] = useState(true);
  const [showResult, setShowResult] = useState(haveSameNumberOfValues);

  const { startValuesName, endValuesName } = useControls({
    startValuesName: {
      value: VALUES[0].name,
      options: VALUES.map((d) => d.name)
    },
    endValuesName: { value: VALUES[1].name, options: VALUES.map((d) => d.name) }
  });

  useEffect(() => {
    const startValuesItem = getValuesFromName(startValuesName);
    const endValuesItem = getValuesFromName(endValuesName);
    const startValues = startValuesItem.values;
    const endValues = endValuesItem.values;

    const values = [startValues, endValues];
    const valuesLenght = values.map((path) => path.length);
    const haveValuesTheSameNumberOfItems = valuesLenght.every(
      (pathLength) => pathLength === valuesLenght[0]
    );
    setHaveSameNumberOfValues(haveValuesTheSameNumberOfItems);
    setShowResult(haveValuesTheSameNumberOfItems);
  }, [startValuesName, endValuesName]);

  console.log("n");
  console.log("haveSameNumberOfValues:", haveSameNumberOfValues);
  console.log("showResult:", showResult);

  return (
    <div className="w-screen h-screen flex flex-col justify-center items-center">
      {!haveSameNumberOfValues && <div>:( Error.</div>}

      {showResult && (
        <Result
          startValues={getValuesFromName(startValuesName)}
          endValues={getValuesFromName(endValuesName)}
        />
      )}
    </div>
  );
};

Result.jsx:

export const Result = ({ startValues, endValues }) => {
  console.log("startValues:", startValues.values.length);
  console.log("endValues:", endValues.values.length);

  return (
    <div className="border-4 border-green-400 px-5 py-3">
      <div>:)</div>
      <div>{startValues.name}</div>
      <div>{endValues.name}</div>
    </div>
  );
};

data.js:

export const VALUES = [
  {
    name: "carrot (3)",
    values: [0, 4, 45]
  },
  {
    name: "apple (3)",
    values: [20, 20, 10]
  },
  {
    name: "salad (4)",
    values: [30, 0, 2, 1]
  },
  {
    name: "chicken (6)",
    values: [40, 1, 3, 20, 3, 1]
  }
];

export function getValuesFromName(name) {
  return VALUES.find((d) => d.name === name);
}

The problem is that when user selects two items with values length not equals (for example Carrot and Chicken), the code set showResult as true so the Result component is rendered even if it shouldn’t.
You can checkit reading the log messagges.
I’m trying to explain myself better using an entire example flow.

  1. refresh page, the selected items are carrot (3) and apple (3). The values have the same lenght and in the console you can see:
haveSameNumberOfValues:  true
showResult: true
startValues: 3
endValues: 3

showResult is true so the Result component is rendered. Ok, it works

  1. user selects chiken (6) as endValuesName. the console prints:
haveSameNumberOfValues: true
showResult: true
startValues: 3
endValues: 6

haveSameNumberOfValues: false
showResult: false

showResult is true the first time, so the Result component is rendered and then it changes and become false. It’s strange because I don’t want that, I’d like to have immediatly showResult=false. This because in my simple example, that do not cause a big problem but in my real application it breaks the app.

What’s wrong with my code?

I repeat what I would like to have:

user changes values using Leva -> showResult should be updated in the right way the first time, before call Result

2

Answers


  1. The problem with your code is that the state variable showResult is updated asynchronously due to the use of useState hook. When you set showResult to true in the initial state, it takes effect immediately and renders the Result component. Later, in the useEffect hook, you set showResult to haveValuesTheSameNumberOfItems, but this update takes time to be reflected in the state.

    To solve this issue, you can use a separate state variable to keep track of whether the data is ready to be shown or not. You can initialize this state variable to false and update it in the useEffect hook along with haveSameNumberOfValues. Once both the state variables are ready, you can set showResult to true.

    Login or Signup to reply.
  2. Render is called before useEffect

    This is the nature of React you cannot change it!. learn more about using useEffect hook and react lifecycle methods.

    What’s wrong with my code?

    Basically nothing. your code is fine, however it can be better


    Here is a brief explanation:

    What you should keep in mind is that when you update a state, this latter does not updates immediately but when the code finishes executing the component rerenders, and only when this happens, the value of the state is updated.
    Then when the component has finished rendering, if there is a useEffect hook, it will look inside its dependency array and check if one or more of those values is different from what it was durring the last render and if its the case useEffect runs (it will do anyway during the first render) and if the code included inside the useEffect hook is updating a state, then this will lead to a new render, so the component rerenders again, that’s why you got this output in the console :

    // component rerendered 
    haveSameNumberOfValues: true
    showResult: true
    startValues: 3
    endValues: 6
    // useEffet is called (because endValues was 3 and become 6), and since inside useEffect you are updating the state (setShowResult(x) and setHaveSameNumberOfValues(y)) the component rerenders again
    // new render
    haveSameNumberOfValues: false
    showResult: false
    // <Result/> is not displayed because showResult is false
    // useEffet is not called (because startValuesName and endValuesName are not updated)
    

    Now, that being said, you understood "the issue", what you have to do, is to avoid triggering useEffect to update haveSameNumberOfValues and showResult, there is no need for that, also this will avoid this extra render.

    Since useControls of Leva is "similar" to the react useState hook each time its value is updated the component rerenders so you’ll have the updated values in this new render. then, when it comes to JSX to display the ui, you call this function (it returns either TRUE or FALSE) and based on its result you decide either to show <Result /> or <div>Error</div>

    The function includes the same logic of your useEffect :

    function haveValuesTheSameNumberOfItems(first, second) {
        const startValuesItem = getValuesFromName(first);
        const endValuesItem = getValuesFromName(second);
        const startValues = startValuesItem.values;
        const endValues = endValuesItem.values;
        const values = [startValues, endValues];
        const valuesLenght = values.map((path) => path.length);
        return valuesLenght.every((pathLength) => pathLength === valuesLenght[0]);
      }
    

    Full working example:

    import { useControls } from "leva";
    const VALUES = [
      {
        name: "carrot (3)",
        values: [0, 4, 45],
      },
      {
        name: "apple (3)",
        values: [20, 20, 10],
      },
      {
        name: "salad (4)",
        values: [30, 0, 2, 1],
      },
      {
        name: "chicken (6)",
        values: [40, 1, 3, 20, 3, 1],
      },
    ];
    const Result = ({ startValues, endValues }) => {
      console.log("startValues:", startValues.values.length);
      console.log("endValues:", endValues.values.length);
    
      return (
        <div className="border-4 border-green-400 px-5 py-3">
          <div>{startValues.name}</div>
          <div>{endValues.name}</div>
        </div>
      );
    };
    
    const App = () => {
      function getValuesFromName(name) {
        return VALUES.find((d) => d.name === name);
      }
    
      function haveValuesTheSameNumberOfItems(first, second) {
        const startValuesItem = getValuesFromName(first);
        const endValuesItem = getValuesFromName(second);
        const startValues = startValuesItem.values;
        const endValues = endValuesItem.values;
        const values = [startValues, endValues];
        const valuesLenght = values.map((path) => path.length);
        return valuesLenght.every((pathLength) => pathLength === valuesLenght[0]);
      }
    
      const { startValuesName, endValuesName } = useControls({
        startValuesName: {
          value: VALUES[0].name,
          options: VALUES.map((d) => d.name),
        },
        endValuesName: {
          value: VALUES[1].name,
          options: VALUES.map((d) => d.name),
        },
      });
    
      return (
        <div className="w-screen h-screen flex flex-col justify-center items-center">
          {haveValuesTheSameNumberOfItems(startValuesName, endValuesName) ? (
            <Result
              startValues={getValuesFromName(startValuesName)}
              endValues={getValuesFromName(endValuesName)}
            />
          ) : (
            <div>: Error.</div>
          )}
        </div>
      );
    };
    export default App;
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search