I have an array of items declared using the useState
hook:
const [items, setItems] = React.useState([{id: 1, title: "Example Item"}, {id: 2, title: "Another example item"}])
I have mapped the values of this array using the items.map
function:
<ul>
{items.map(item => (
<li key={item.id}>{item.title}</li>
))}
</ul>
I have a couple of buttons to sort the array of items:
<p><button onClick={sortByTitleAscending}>Sort by ascending</button></p>
<p><button onClick={sortByTitleDescending}>Sort by descending</button></p>
I use the setItems
function to update the list of items after sorting it on onClick
of the button.
const sortByTitleAscending = () => {
setItems((prevItems) => {
const sortedItems = prevItems.sort((a, b) => a.title.localeCompare(b.title));
console.log(sortedItems);
return sortedItems;
});
}
const sortByTitleDescending = () => {
setItems((prevItems) => {
const sortedItems = prevItems.sort((a, b) => b.title.localeCompare(a.title));
console.log(sortedItems);
return sortedItems;
});
}
The following is a stack snippet of my code:
function App() {
const [items, setItems] = React.useState([{id: 1, title: "Example Item"}, {id: 2, title: "Another example item"}])
const sortByTitleAscending = () => {
setItems((prevItems) => {
const sortedItems = prevItems.sort((a, b) => a.title.localeCompare(b.title));
console.log(sortedItems);
return sortedItems;
});
}
const sortByTitleDescending = () => {
setItems((prevItems) => {
const sortedItems = prevItems.sort((a, b) => b.title.localeCompare(a.title));
console.log(sortedItems);
return sortedItems;
});
}
return (
<div>
<h1>Hello World!</h1>
<p><button onClick={sortByTitleAscending}>Sort by ascending</button></p>
<p><button onClick={sortByTitleDescending}>Sort by descending</button></p>
<ul>
{items.map(item => (
<li key={item.id}>{item.title}</li>
))}
</ul>
</div>
);
}
ReactDOM.render(<App/>, document.getElementById('root'));
<div id="root"></div>
<script src=".3.1/umd/react.production.min.js"></script>
<script src=".3.1/umd/react-dom.production.min.js"></script>
I have an array of items declared using the useState
hook:
const [items, setItems] = React.useState([{id: 1, title: "Example Item"}, {id: 2, title: "Another example item"}])
I have mapped the values of this array using the items.map
function:
<ul>
{items.map(item => (
<li key={item.id}>{item.title}</li>
))}
</ul>
I have a couple of buttons to sort the array of items:
<p><button onClick={sortByTitleAscending}>Sort by ascending</button></p>
<p><button onClick={sortByTitleDescending}>Sort by descending</button></p>
I use the setItems
function to update the list of items after sorting it on onClick
of the button.
const sortByTitleAscending = () => {
setItems((prevItems) => {
const sortedItems = prevItems.sort((a, b) => a.title.localeCompare(b.title));
console.log(sortedItems);
return sortedItems;
});
}
const sortByTitleDescending = () => {
setItems((prevItems) => {
const sortedItems = prevItems.sort((a, b) => b.title.localeCompare(a.title));
console.log(sortedItems);
return sortedItems;
});
}
The following is a stack snippet of my code:
function App() {
const [items, setItems] = React.useState([{id: 1, title: "Example Item"}, {id: 2, title: "Another example item"}])
const sortByTitleAscending = () => {
setItems((prevItems) => {
const sortedItems = prevItems.sort((a, b) => a.title.localeCompare(b.title));
console.log(sortedItems);
return sortedItems;
});
}
const sortByTitleDescending = () => {
setItems((prevItems) => {
const sortedItems = prevItems.sort((a, b) => b.title.localeCompare(a.title));
console.log(sortedItems);
return sortedItems;
});
}
return (
<div>
<h1>Hello World!</h1>
<p><button onClick={sortByTitleAscending}>Sort by ascending</button></p>
<p><button onClick={sortByTitleDescending}>Sort by descending</button></p>
<ul>
{items.map(item => (
<li key={item.id}>{item.title}</li>
))}
</ul>
</div>
);
}
ReactDOM.render(<App/>, document.getElementById('root'));
<div id="root"></div>
<script src="https://cdnjs.cloudflare/ajax/libs/react/18.3.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js"></script>
As you can see, when you run the code and click on "Sort by ascending" or "Sort by descending", the mapped list doesn't get updated, but the list of sorted items is shown in the console.
I would like to know why this happens and also what is the correct way to sort a list of mapped items.
Share Improve this question edited Feb 15 at 17:32 Drew Reese 203k17 gold badges237 silver badges268 bronze badges asked Feb 15 at 8:39 HarishHarish 5282 gold badges11 silver badges19 bronze badges4 Answers
Reset to default 4The issue with your code lies in the sorting operation:
const sortedItems = prevItems.sort((a, b) => a.title.localeCompare(b.title));
Here, you're operating on the original prevItems
array by sorting it in place (Array.prototype.sort()
gives a reference to the modified array). When setItems is called with the sorted array, React does not recognize that the state has changed because the reference to the array remains the same. This results in the UI not updating to reflect the sorted order.
You can solve this by copying the prevItems
to ensure you're not dealing with references anymore but operating with the array values:
const sortedItems = [...prevItems].sort((a, b) => a.title.localeCompare(b.title));
You can read more about state immutability in the React Docs. Hope this helps!
This is due the reference memory. You are only modifying the contents inside array and reference still same, and react wont identify the change and it will not re render. The older react version may re render this but with >18 it wont re render unless there is a change in state.
Try this
function App() {
const [items, setItems] = React.useState([{id: 1, title: "Example Item"}, {id: 2, title: "Another example item"}])
const sortByTitleAscending = () => {
setItems((prevItems) => {
const sortedItems = [...prevItems.sort((a, b) => a.title.localeCompare(b.title))];
console.log(sortedItems);
return sortedItems;
});
}
const sortByTitleDescending = () => {
setItems((prevItems) => {
const sortedItems = [...prevItems.sort((a, b) => b.title.localeCompare(a.title))];
console.log(sortedItems);
return sortedItems;
});
}
return (
<div>
<h1>Hello World!</h1>
<p><button onClick={sortByTitleAscending}>Sort by ascending</button></p>
<p><button onClick={sortByTitleDescending}>Sort by descending</button></p>
<ul>
{items.map(item => (
<li key={item.id}>{item.title}</li>
))}
</ul>
</div>
);
}
ReactDOM.render(<App/>, document.getElementById('root'));
<div id="root"></div>
<script src="https://cdnjs.cloudflare/ajax/libs/react/18.3.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js"></script>
As others have stated, it's because the .sort()
method returns a reference to the original array (but now with sorted), so React doesn't know that it needs to rerender.
Rather than creating a clone of your data with the spread syntax and then sorting, you can instead use the more modern array method: .toSorted()
, which sorts and creates a new array reference in one go (theoretically being more efficient):
const sortedItems = prevItems.toSorted((a, b) => a.title.localeCompare(b.title));
The issue is array mutation, which has been detailed here. The table given over there may be a useful reference when dealing with array states. This issue in the code has been rightly spotted and detailed by the two answers.
This post is trying to answer the below part of the question,
...what is the correct way to sort a list of mapped items...
Mostly it has been seen done as:
You may copy the state and chain sort prior to map as the below code does. This code uses an additional state to hold the sort order which is true by default and toggled on button click.
...
<ul>
{[...items]
.sort((a, b) =>
sortOrder
? a.title.localeCompare(b.title)
: b.title.localeCompare(a.title)
)
.map((item) => (
<li key={item.id}>{item.title}</li>
))}
..