Reputation: 23
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} </span>
<button onClick={() => handleRemove(item.id)}>×</button>
</li>
);
})
}
</ul>
What i'm missing here?
Upvotes: 2
Views: 76
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:
array.sort
workuseState
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:
is
above to check for state changes when using hooksUpvotes: 1
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.
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} </span>
<button>×</button>
</li>
))}
</ul>
</>
);
};
Upvotes: 2
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} </span>
<button onClick={() => handleRemove(item.id)}>×</button>
</li>
))}
</ul>
</>
);
}
Hope this helps 🙂
Upvotes: 1