2

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 4

4
+50

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

Sign up to request clarification or add additional context in comments.

7 Comments

"setList check if given value is different from previous then render". Could you reference this?
From my knowledge, even if you setList(previous=>previous) it still will trigger a re-render
setList(previous=>previous) will not trigger re-render as we see in this sandbox: codesandbox.io/s/…
about the quote, you mentioned i couldn't find a valid reference. i reached this by Trial and error.
Thanks for the insight. I found the reference and posted in the question. I think the API documentation should be more explicit.
|
2

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

1 Comment

Sorry, the console.log is still different from what is displayed.
1

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

11 Comments

I have updated to include list as a dependency but it didn't seem to have any effect on the code snippet in the original question.
You also have a bug in the sort callback, check my example
Sorry. I don't see any bugs in my question
Your answer is wrong. If you add console.log(list) to line 14, you will notice that the list logged and the list rendered are different.
To log stuff you don't put it in the render function... Log it in useEffect
|
1

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! ;)

2 Comments

Thanks for the response, but that does not explain why console.log has a different output than what is shown on html.
Running useState and useReducer in parallel certainly is the reason... React does state updates in batch... It would hazardous for to explain .oO(lol) but read that

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.