My Levels component contains a list of Level components that gets rendered in the UI.
A level is basically a list of items.
From my Levels component, I pass a function to my Level component. Inside the Level component there is a button, when clicked, will call the function I passed from my Levels component called createInnerRange
.
So when you click the button, it creates a new list of items. The new list that was just created also has that button, when clicked should create another list.
The bug is, when the newly created list is rendered, and I click the button to create a new list, it creates a new list but it overwrites the first list I that was just created.
Why is this happening? I am always appending to the Levels state whenever a new Level is created.
import React, { ReactElement, useEffect, useState } from 'react';
export interface LevelProps {
innerRangeHandler?: (a: number, b: number) => void;
}
export interface LevelsState {
levels: Array<LevelProps>;
};
const initialState: LevelsState = {
levels: [],
};
function Levels() {
const [state, setState] = useState<LevelsState>(initialState);
const addLevel = (props: LevelProps) => {
console.log('add level called');
setState({
...state,
levels: [...state.levels, props]
})
}
const newLevelOnClick = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
addLevel({name: "none"});
}
const createInnerRange = (high: number, low: number) => {
addLevel({ name: `inner-${high}:${low}`, high: high, low: low, innerRangeHandler: createInnerRange});
}
return (
<div>
<div className="flex flex-row">
<div className="flex-[3]">
<div className="flex flex-row">
{state.levels.map((props, i) =>
<div className="m-1" key={`${props.name}-${i}`}><Level {...props} /></div>
)}
</div>
</div>
</div>
</div>
);
}
export default Levels;
my Level component has a button, the onClick handler looks like:
const levelButton = (index: number) => (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
const button: HTMLButtonElement = event.currentTarget;
console.log(`clicked level with index=${index}`);
props.innerRangeHandler!(a, b);
}
CodePudding user response:
Every single function that is defined within a functional component, is in theory, only defined on mount, therefore the parameters passed into it are not going to be updated from your state updates. The state of your app is correctly updating, however the values your functions are connected to were defined on mount, and on render everything EXCEPT your functions are updated. There are 2 work arounds to this situation, one is ONLY applicable to useState
, and the other is by utilizing useCallback
setState
allows you to pass in a function, which will ensure its returning the most up to date currentState value when its updating the state value.
const addLevel = (props: LevelProps) => {
console.log('add level called');
setState(currentState => ({
...currentState,
levels: [...currentState.levels, props]
});
}
useCallback is incredibly similar to useEffect in that its watching the data that lives within the component, and when it detects a change to one of its dependencies it updates the values associated with the function to make sure the functions references are the most up to date references.
const addLevel = (props: LevelProps, currentState: any) => {
console.log('add level called');
setState({
...currentState,
levels: [...currentState.levels, props]
});
}
const newLevelOnClick = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
addLevel({ name: 'none' }, state);
},
[state]
);
In theory, I think just having the correct state from the addLevel function is enough to solve your issue. But useCallback is the appropriate way to define a function that has potentially changing data, it may be an over optimization in this context, but someone may into this issue again, so I wanted to give a well rounded solution to THE problem rather than just your problem