donglee
donglee

Reputation: 23

Not rendering while state is changed

I'm learning react. I am trying to sort a list based on name. The ShoppingList component is

const ShoppingList = () => {
    const [items, setItems] = useState([]);

    const data = [
        {id: 1, name: 'Soda'},
        {id: 2, name: 'ice'},
    ];

    useEffect(() => {
        setItems(data);
    }, []);
    
    const handleSort = () => {}
   return ();

}

On a button click I'm trying to sort the data and display it.

<button onClick={() => handleSort()}>Sort by name</button>

Inside the handleSort() function

const sortItems = items.sort((a, b) => {
    const nameA = a.name.toUpperCase();
    const nameB = b.name.toUpperCase();
    if(nameA < nameB)
        return -1;
    if(nameA > nameB)
        return 1;
    return 0;
});

console.log(sortItems);
setItems(sortItems);

The console.log(sortItems) shows the sorted array. But not rendering in the DOM. Inside the return, I'm trying to display the sorted data in this format

<ul>
    {items.map((item) => {
        return (
            <li key={item.id}>
              <span>{item.name}&nbsp;</span>
              <button onClick={() => handleRemove(item.id)}>&times;</button>
            </li>
        );
        })
     }
</ul>

What i'm missing here?

Upvotes: 2

Views: 76

Answers (3)

somallg
somallg

Reputation: 2033

If you are interested to know more indepth on why the array items is changed (sorted) but React doesn't render, there are 2 things to take note:

  1. How array.sort work
  2. How React re-render with useState

For (1), it's easy, array.sort return the sorted array. Note that the array is sorted in place, and no copy is made. Hence sortItems and items still refer to the same array

For (2), it's a little bit complicated as we have to read through React code base.

This is React.useState signature

export function useState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

Use Github navigation tools and scan we gonna end-up to this code:

useState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  currentHookNameInDev = 'useState';
  mountHookTypesDev();
  const prevDispatcher = ReactCurrentDispatcher.current;
  ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
  try {
    return mountState(initialState);
  } finally {
    ReactCurrentDispatcher.current = prevDispatcher;
  }
}

Next we must find the definition of mounState:

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  ...
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}

Notice, the return type of mountState is an array where the 2nd argument is an function just like const [items, setItems] = useState([]) Which means we almost there. dispatch is the value from dispatchAction.bind

Scan through the code we gonna end up at this line:

      if (is(eagerState, currentState)) {
        // Fast path. We can bail out without scheduling React to re-render.
        // It's still possible that we'll need to rebase this update later,
        // if the component re-renders for a different reason and by that
        // time the reducer has changed.
        return;
      }

Last part is what function is does:

function is(x: any, y: any) {
  return (
    (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
  );
}

It simply check for equality using === operator.

Comeback to your sort function, nextState in our case is sortItems and prevState is items. With (1) in mind, sortItems === items => true so React gonna skip the rendering. That's why you see most of the tutorials states that you have to do shallow copy. By doing so your nextState will differ from your prevState.

TLDR:

  1. React use function is above to check for state changes when using hooks
  2. Always make shallow copy when working with array, object if you are using hooks

Upvotes: 1

AKX
AKX

Reputation: 168843

I'd recommend deriving the sorted list of items with useMemo, so it's "derived state" dependent on the items array and the desired sort order.

  • Don't use useEffect for initial state. useState accepts a creator function for the initial state instead.
  • localeCompare is a neater way to return -1, 0, +1 for comparison.
  • [...items] (a shallow copy of items) is required, because .sort() sorts an array in-place.
const sortByName = (a, b) => a.name.toUpperCase().localeCompare(b.name.toUpperCase());

const ShoppingList = () => {
  const [items, setItems] = useState(() => [
    { id: 1, name: "Soda" },
    { id: 2, name: "ice" },
  ]);
  const [sortOrder, setSortOrder] = useState("original");
  const sortedItems = React.useMemo(() => {
    switch (sortOrder) {
      case "byName":
        return [...items].sort(sortByName);
      default:
        return items;
    }
  }, [items, sortOrder]);

  return (
    <>
      <button onClick={() => setSortOrder("byName")}>Sort by name</button>
      <button onClick={() => setSortOrder("original")}>Sort in original order</button>
      <ul>
        {sortedItems.map((el, i) => (
          <li key={el.id}>
            <span>{el.name}&nbsp;</span>
            <button>&times;</button>
          </li>
        ))}
      </ul>
    </>
  );
};

Upvotes: 2

Alixsep
Alixsep

Reputation: 400

First of all you need to stop using useEffect for the initial state, And if you want react to notice your changes, use an object instead of array. (This is not always that react doesn't notice your changes, but since you didn't change array and it was only sorted, react ignores it).

const ShoppingList = () => {
    const [items, setItems] = useState({
      data: [
        { id: 1, name: 'Soda' },
        { id: 2, name: 'ice' },
      ],
    });
    const handleSort = () => {
      const sortedItems = items.data.sort((a, b) => {
        const nameA = a.name.toUpperCase();
        const nameB = b.name.toUpperCase();
        if (nameA < nameB) return -1;
        if (nameA > nameB) return 1;
        return 0;
      });
      setItems({
        data: sortedItems,
      });
    };
    return (
      <>
        <button onClick={() => handleSort()}>Sort by name</button>
            <ul>
                {items.data.map((el, i) => (
                    <li key={el.id}>
                        <span>{el.name}&nbsp;</span>
                        <button onClick={() => handleRemove(item.id)}>&times;</button>
                    </li>
                 ))}
            </ul>
       </>
    );
}

Hope this helps 🙂

Upvotes: 1

Related Questions