Reputation: 69
I have an issue where one setState
function is updating two separate state values.
I am trying to make a small React sortable array. The program should behave like so:
unsortedData
& displayedData
state hooksdisplayedData
is sorted (using .sort()
)displayedData
to be the same value as unsortedData
- restoring the original orderHowever, on first click of toggle sorting, it sorts both unsortedData
& displayedData
, which means I lose the original data order. I understand I could store their order, but I'm wanting to know why these state values appear to be coupled.
Stackblitz working code here.
GIF showing issue here
I cannot find where this is happening. I can't see any object/array referencing going on (I'm spreading for new objects/arrays).
Code here:
const Test = () => {
const [unsortedData, setUnsortedData] = useState([])
const [displayedData, setDisplayedData] = useState([])
const [isSorted, setisSorted] = useState(false)
const handleSorting = () => setisSorted(!isSorted)
useEffect(() => {
if (isSorted === true) setDisplayedData([...unsortedData.sort()]) // sort data
if (isSorted === false) setDisplayedData([...unsortedData]) // restore original data order
}, [isSorted, unsortedData])
useEffect(() => {
const mockData = [3, 9, 6]
setUnsortedData([...mockData]) // store original data order in "unsortedData"
setDisplayedData([...mockData])
}, [])
return (
<div>
{displayedData.map(item => item)}
<br />
<button onClick={() => handleSorting()}>Toggle sorting</button>
</div>
)
}
Upvotes: 0
Views: 71
Reputation: 85211
The .sort
function mutates the array, so when you do this:
setDisplayedData([...unsortedData.sort()])
You are mutating unsortedData, then making a copy of it afterwards. Since you mutated the original array, that change can display on the screen when the component rerenders.
So a minimum fix would be to copy first, and sort afterwards:
setDisplayedData([...unsortedData].sort())
Also, why do I need the useEffect? Why can't I just move the contents of the useEffect (that has the if statements in) into the handleSorting function?
Moving it into handleSorting should be possible, but actually i'd like to propose another option: have only two states, the unsorted data, and the boolean of whether to sort it. The displayed data is then a calculated value based on those two.
const [unsortedData, setUnsortedData] = useState([3, 9, 6]) // initialize with mock data
const [isSorted, setisSorted] = useState(false)
const displayedData = useMemo(() => {
if (isSorted) {
return [...unsortedData].sort();
} else {
return unsortedData
}
}, [unsortedData, isSorted]);
const handleSorting = () => setisSorted(!isSorted)
// No use useEffect to sort the data
// Also no useEffect for the mock data, since i did that when initializing the state
The benefits of this approach are that
Upvotes: 1
Reputation: 91
If you want to display sorted array to user only when the user presses the button, you could use this code statement (orignaly coppied from vue3 todo list)
const filters = {
none: (data) => [...data],
sorted: (data) => [...data].sort((a, b) => a - b),
}
const Test = () => {
const [unsortedData, setUnsortedData] = useState([])
const [currentFilter, setCurrentFilter] = useState('none')
useEffect(() => {
const mockData = [3, 9, 6]
setUnsortedData([...mockData])
}, [])
return (
<div>
<ul>
{/* All magic is going here! */}
{filters[currentFilter](unsortedData).map(item => <li key={item}>{item}</li>)}
</ul>
<br />
<button
onClick={() =>
setCurrentFilter(currentFilter === 'none' ? 'sorted' : 'none')}>
Toggle sorting
</button>
</div>
)
}
Upvotes: 0