State is not updating from within the child function after the first click in React Typescript.
App.tsx
const list: { name: string, age: number, username: string }[] = [
{
name: "Robert",
age: 20,
username: "rtucker"
},
{
name: 'John',
age: 30,
username: 'jtest'
},
{
name: "Alice",
age: 24,
username: 'acooper'
},
]
function App() {
const [activeList, setActiveList] = useState(list);
const didMount = useRef(false)
useEffect(() => {
setActiveList(list)
}, [])
useEffect(() => {
// Return early, if this is the first render:
if (!didMount.current) {
didMount.current = true;
return
}
// Paste code to be executed on subsequent renders:
console.log(activeList)
}, [activeList])
const SortList = (item: string): void => {
let sortedList = new Array<typeof list[0]>()
//console.log(options)
let index = options.findIndex(o => o === item)
console.log(index, item)
switch (item) {
case "age":
if (index === -1) {
sortedList = list.sort((a, b) => {
//console.log(a.name, b.name)
if (a.age > b.age) return 1
if (a.age < b.age) return -1
return 0
})
options.push(item)
console.log('ascending', item)
} else {
sortedList = list.sort((a, b) => {
if (a.age < b.age) return 1
if (a.age > b.age) return -1
return 0
})
console.log('descending')
console.log(index)
options.splice(index, 1)
}
console.log(sortedList)
break
default:
break
}
setActiveList(sortedList)
}
const element = document.getElementById('root')
return (
<><div id='menu-container'>
<ul style={{ listStyle: 'none' }}>
{activeList.map((i, index) => (
<li key={index}><p>name: {i.name} <br />age: {i.age} <br /> username: {i.username}</p></li>))}
</ul><ContextMenu title={'Sort'} items={items} onClick={SortList} element={element} />
</div></>
)
}
ContextMenu.tsx
interface IContextMenu {
title: string
items: Array<string>
onClick: (item: string) => void
element: HTMLElement | null
}
export default function ContextMenu({ title, items, onClick, element }: IContextMenu) {
if (!element) return null
const { anchorPoint, isOpen, setIsOpen } = useContextMenu(element);
if (!isOpen) {
return null;
}
return (
<><ul
className={styles.ContextMenu}
style={{ top: anchorPoint.y, left: anchorPoint.x }}
><p className={styles.title}>{title}</p>
{items.map((item) => (
<li key={item} onClick={() => {
onClick(item)
setIsOpen(false)
}}>{item}</li>
))}
</ul></>
);
}
useContextMenu.ts file
function useContextMenu (element: HTMLElement) {
const [isOpen, setIsOpen] = useState(false)
const [anchorPoint, setAnchorPoint] = useState<AnchorPoint>({ x: 0, y: 0 })
// const width = window.innerWidth
const handleContextMenu = useCallback(
(event: MouseEvent) => {
event.preventDefault()
setAnchorPoint({
x: event.pageX,
y: event.pageY
})
setIsOpen(true)
},
[setIsOpen, setAnchorPoint]
)
const handleClick = useCallback(() => {
if (isOpen) {
setIsOpen(false)
}
}, [isOpen])
useEffect(() => {
element.addEventListener('click', handleClick)
element.addEventListener('contextmenu', handleContextMenu)
return () => {
element.removeEventListener('click', handleClick)
element.removeEventListener('contextmenu', handleContextMenu)
}
})
return {
anchorPoint,
isOpen,
setIsOpen
}
}
contextMenu.scss
.ContextMenu {
position: fixed;
background: #eee;
border: 1px solid #ccc;
border-radius: 0.3rem;
top: 0;
left: 0;
max-width: 15rem;
list-style: none;
margin: 0;
.title {
border-bottom: 1px solid #ccc;
text-align: center;
}
& li {
margin: 0;
padding: 0;
padding-right: 0.25rem;
font-size: 0.9em;
cursor: pointer;
}
& li:hover,
& li:focus {
background-color: grey;
color: white;
}
// & li:not(:last-child) {
// margin: 0 0 1rem;
// }
}
What is supposed to happen is when the SortList function receives a string ‘age’ it creates a sorted array from the list variable on the first click in ascending order and descending array on the second click.
For some reason the first time it’s clicked to sort all is good and everything updates however that’s the last time it updates. It will not sort in reverse order as the ActiveList never updates after the first time.
3
Answers
I was ABLE TO FIND A SOLUTION!!!
You are taking the initial state (
list
), mutate it by sorting it, and then set it back to the state.Since the current and previous state are the same array (just sorted), React doesn’t detect the change, and doesn’t re-render the component.
Whenever you sort the list, clone the original array using spread, and then sort it:
Right now the sorted list is always based on the initial value, so any change to list (adding / removing items for example), would reset when sorting. I would clone the previous state, and then sort it:
Notes:
You already use
list
as the initial state, so setting it inuseEffect
is redundant:You are also mutation the
options
array, and this might also affect rendering ifoptions
is a state. Theses statements mutateoptions
:It looks like you’re setting the list items’
key
prop as the array index, but the indices won’t change when the list is sorted. You should choose another attribute for eachkey
(such asname
) so that React can "see" the change in order:As the docs say: "Index as a key often leads to subtle and confusing bugs."
https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key