React, console.log and render display different values

Viewed 1293

It appears that the object logged to console does not match what is displayed on the HTML, I'm puzzled by this simple example below. Here the button toggles the ordering of the array, and React seems to render the previous list.

=== Update 3 ====

The cuprite is object compassion where setState(list=>list.push(12)) will mutate the list but won't trigger setState because the id of the list is still the same.

If you update a State Hook to the same value as the current state, React will bail out without rendering the children or firing effects. (React uses the Object.is comparison algorithm.) React useState doc

=== Update 2 ====

I think I found the solution. The culprit seems to be the mutability of objects, and the workaround is to create a deep copy of the sorted list in Solution Sandbox. I believe I found an undefined behaviour of React, since the mutability of list should not cause any differences in console.log(list) and <h1>list</h1> shown in the image below. Correct modification of state arrays in ReactJS

enter image description here

=== Update 1 ====

Sandbox which contains the same code as the one shown below.

There is nothing wrong with my sort function! If ascending, then the smaller element should come first, which means the comparison function (e1,e2)=>e1-e2 is correct.

If compareFunction(a, b) returns less than 0, sort a to an index lower than b (i.e. a comes first). Comparision function Mozilla

const {useState, useEffect, useReducer} = React;

function App() {
  const [list, setList] = useState([0,1]);
  const [isAscending, toggle] = useReducer(isAscending=>!isAscending, true);
  // update the order of the list
  useEffect(()=>{
    if (isAscending) {
      setList(list.sort((e1,e2)=>e1-e2));
    } else {
      setList(list.sort((e1,e2)=>e2-e1));
    }
  },[list, isAscending]);
  // render
  return (
    <div>
    {console.log("list[0] should be",list[0])}
    <button onClick={toggle}>{isAscending?"Ascending":"Descending"}</button>
    <h1>list[0]: {list[0]}</h1>
    </div>
  )
}

ReactDOM.render(<App/>, document.querySelector('.App'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div class='App'/>

4 Answers

according to MDN javascript sort function mutates array.

The sort() method sorts the elements of an array in place and returns the sorted array.

when you write:

setList(list.sort((e1, e2) => e1 - e2));

you are mutating list before setList mutate it.

setList check if given value is different from previous then render.

but setList sees no different since the list value is already updated by sort method.

because of this, render seems one step behind.

so you should write:

setList([...list].sort((e1, e2) => e1 - e2));

one more thing:

i think you should change

{isAscending ? "Ascending" : "Descending"}

to

{isAscending ? "Descending" : "Ascending"}

since it is a toggle button

Your initial order is ascending order. When you click the button, the first time for toggling the order you want to move from ascending to descending order.

Which means

if (isAscending) {
    // Logic for descending should come here
    setList(list.sort((e1, e2) => e2 - e1));
} else {
    // Logic for ascending should come here
    setList(list.sort((e1, e2) => e1 - e2));
}

Check the code here and let me know if this is what you were looking for. Code Sandbox: Sorted List is Not Rendering

You have closure on list value at the useEffect callback, you should add it to your dep array:

function App() {
  const [list, setList] = useState([0, 1]);
  const [isAscending, toggle] = useReducer((isAscending) => !isAscending, true);

  useEffect(() => {
    setList(list.sort((e1, e2) => (!isAscending ? e1 - e2 : e2 - e1)));
  }, [list, isAscending]);

  return (
    <div>
      <button onClick={toggle}>
        {isAscending ? "Ascending" : "Descending"}
      </button>
      <pre>{JSON.stringify(list)}</pre>
    </div>
  );
}

Note that if you have eslint you should get a warning on it.

https://codesandbox.io/s/react-template-forked-08eft?file=/index.js:99-684

I simplified your snippet just a bit.

You were strangely using both useState and useReducer while the second is an alternative to the first.

useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one.

Quoted from Reactjs docs

const {useReducer} = React;

function App() {
  const list=[0,1]
  const [isAscending, toggle] = useReducer(isAscending=>{
  
    if (isAscending) {
      list.sort((e1,e2)=>e2-e1)
    } else {
      list.sort((e1,e2)=>e1-e2)
    }

    return !isAscending
  }, list);

  // render
  return (
    <div>
    {console.log("list[0] should be",list[0])}
    <button onClick={toggle}>{isAscending?"Ascending":"Descending"}</button>
    <h1>list[0]: {list[0]}</h1>
    </div>
  )
}

ReactDOM.render(<App/>, document.querySelector('.App'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div class='App'/>

So here list is the initialState of the documentation example. And the logic to re-order the list, which is now real simple, is inside the reducer function.

Hoping that helps! ;)

Related