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'/>
Chuan
  • 429
  • 5
  • 16

4 Answers4

3

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

armin yahya
  • 1,368
  • 7
  • 14
  • "setList check if given value is different from previous then render". Could you reference this? – Chuan Nov 03 '20 at 10:14
  • From my knowledge, even if you `setList(previous=>previous)` it still will trigger a re-render – Chuan Nov 03 '20 at 10:15
  • setList(previous=>previous) will not trigger re-render as we see in this sandbox: https://codesandbox.io/s/check-render-of-list-if-update-to-prev-value-g21ll?file=/src/App.js – armin yahya Nov 03 '20 at 10:52
  • about the quote, you mentioned i couldn't find a valid reference. i reached this by Trial and error. – armin yahya Nov 03 '20 at 10:56
  • Thanks for the insight. I found the reference and posted in the question. I think the API documentation should be more explicit. – Chuan Nov 03 '20 at 20:13
  • @Chuan: Please check if there is any infinite loop with this fix. Since I went thought that "solution" too before changing to my actual answer. – Louys Patrice Bessette Nov 03 '20 at 21:04
  • Yeah. There is an infinite loop because I placed list as the dependency for the useEffect that updates the list. – Chuan Nov 03 '20 at 21:39
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

Darshna Rekha
  • 1,069
  • 7
  • 20
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

Dennis Vash
  • 50,196
  • 9
  • 100
  • 118
  • 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. – Chuan Oct 25 '20 at 06:53
  • You also have a bug in the sort callback, check my example – Dennis Vash Oct 25 '20 at 06:59
  • Sorry. I don't see any bugs in my question – Chuan Oct 25 '20 at 07:19
  • 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. – Chuan Oct 29 '20 at 06:22
  • To log stuff you don't put it in the render function... Log it in `useEffect` – Dennis Vash Oct 29 '20 at 06:24
  • Why you shouldn't log things inside the return value? Could you support your claim? I assume the render function is the return value. – Chuan Oct 30 '20 at 08:07
  • It also makes no sense to log things inside useEffect. Since useEffect is async, when it prints the state might have already changed. Using your example https://codesandbox.io/s/q-64520983-closureinuseeffect-forked-2r6n2 – Chuan Oct 30 '20 at 08:17
  • Please read React docs, this question might help you too: https://stackoverflow.com/questions/59841800/react-useeffect-in-depth-use-of-useeffect/59841947#59841947 – Dennis Vash Oct 30 '20 at 08:38
  • Yo. You just referenced another of your own answer that doesn't explain why you can't log inside JSX. I don't see React doc that says "don't log inside JSX". Here is an answer that logs inside JSX https://stackoverflow.com/questions/40647361/console-logging-for-react – Chuan Oct 30 '20 at 09:00
  • There is a difference between logging inside render function (functional component body) OR inside the Component tree ("inside JSX") like you did, the render function runs on every render (that's what you want) the JSX tree only changes on diff algorithm – Dennis Vash Oct 30 '20 at 09:05
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/223859/discussion-between-chuan-and-dennis-vash). – Chuan Oct 30 '20 at 09:13
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! ;)

Louys Patrice Bessette
  • 33,375
  • 6
  • 36
  • 64
  • Thanks for the response, but that does not explain why console.log has a different output than what is shown on html. – Chuan Nov 03 '20 at 20:01
  • 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](https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous) – Louys Patrice Bessette Nov 03 '20 at 20:16