I have a form that contains several questions. Some of the questions contains a group of subquestions.
{
(this.props.moduleDetails.questions[questionInfo].question_group && this.props.moduleDetails.questions[questionInfo].has_grouped_questions === 1) ?
<div className="sub-questions">
{
(this.props.moduleDetails.questions[questionInfo].question_group ) ?
<span>
{ this.renderQuestionGroup(questionInfo) }
<input type="button" onClick={(e) => { this.addQuestionGroup(questionInfo) }} value="Add" className="btn"/>
</span>
: null
}
</div>
: null
}
As you can see, renderQuestionGroup is the method that displays the sub-questions.
renderQuestionGroup(questionInfo) {
let input = [];
input = this.display(questionInfo)
return input;
}
display(questionInfo) {
let input = [];
this.state && this.state.groupedQuestions && this.state.groupedQuestions[questionInfo] && this.state.groupedQuestions[questionInfo].map(
(qInfo, qIndex) => {
if(qInfo !== undefined) {
Object.keys(qInfo).map(
(questionInfo, index) => {
input.push(<QuestionAnswers
questionInfo={qInfo[questionInfo]}
generateStepData={this.props.generateStepData}
userEnteredValues={this.props.formValues}
key={qInfo[questionInfo].module_question_id+qIndex}
groupIndex={qIndex}
/>)
});
}
});
return input;
}
Subquestions (a group of questions) are placed in state during the componentDidUpdate.
componentDidUpdate = (prevProps, prevState, snapshot) => {
console.log('prevProps----------->', prevProps);
console.log('this.props----------->', this.props)
if (prevProps !== this.props) {
let questions = this.props.moduleDetails.questions,
sgq = {};
Object.keys(questions).map((qId) => {
sgq[qId] = (this.state.groupedQuestions[qId]) ? this.state.groupedQuestions[qId] : [];
let answerId = this.props.formValues[qId],
questionObj = questions[qId],
groupedQuestions = [];
if(questionObj.has_grouped_questions == 1 && answerId != null && (this.state.groupedQuestions != null)) {
groupedQuestions = questions[qId].question_group[answerId];
let loopCount = this.getLoopCount(groupedQuestions);
for(let i=0; i<loopCount; i++) {
sgq[qId].push(groupedQuestions);
}
}
});
this.setState({groupedQuestions: sgq});
}
}
This works well and the questions get loaded correctly initially. The problem is that during every handleChange event, the method componentDidUpdate is getting triggered which eventually leads to rendering questions again.
I dont want the componentDidUpdate method to invoke during the handlechange event. Any idea on how to fix this?
PS: My idea was to compare prevProps and this.props. If both are not same, it will be handle change event. But the issue is that if i remove the condition
if (prevProps !== this.props) {
...
}
I am getting an infinite loop during initial load. So what i need is as follows.
Detect handleChange inside componentDidUpdate method.
Questions to render correctly initially. Currently i am getting an infinite loop if i remove the if (prevProps !== this.props) { ...} condition.
Any help would be appreciated.
componentDidUpdate is triggered on each change of the state, this is why you can get infinite loops when you set the state there.
If your new data is coming from from props then move this logic to getderivedstatefromprops (or componentWillReceiveProps if you are using lower version of react below v16.3).
getderivedstatefromprops
componentWillReceiveProps
As for handle change just set the state inside the handler, no need to listen for changes in componentDidUpdate.
Related
I'm creating a Tetras game with react, all was working well and executing as expected till is added a Keyboard EventListener which majorly listens for the the Right and Left arrow key to move the component
The Problem is, every time I call setState() in the function i assigned to the event it keeps repeating the same setState() function, without it even rendering the component, as a result i get the "Maximum update depth exceeded" Error.
the code for my component:
class App extends React.Component {
constructor () {
super()
this.state = {
allBlocks: [],
allBlockClass: []
}
setInterval(this.newBlock, 5000)
document.addEventListener('keyup', this.userInput)
}
userInput = e => {
switch (e.key) {
case 'ArrowLeft': {
this.moveLeft()
break
}
case 'ArrowRight': {
// this.moveRight()
break
}
default: {
console.log('either left or right key')
}
}
}
moveLeft = () => {
this.setState(prevState => {
const old_state = Object.assign([], prevState.allBlocks)
let newBlocksClass = prevState.state.allBlockClass
const new_state = old_state.map((blockData, index) => {
if (blockData.active) {
blockData.x_axis -= BLOCK_INCREMENT
if (!(index > this.state.allBlockClass.length - 1)) {
newBlocksClass[index].moveBlockLeft(BLOCK_INCREMENT)
}
}
return blockData
})
return { allBlocks: new_state, allBlockClass: newBlocksClass }
})
}
newBlock = () => {
let positions = generateBlockPositions()
const blockData = {
positions: positions,
active: true,
x_axis: axis_props.Min_x_axis + BLOCK_INCREMENT * randomInt(3),
y_axis: axis_props.Min_y_axis + BLOCK_INCREMENT
}
this.setState(prevState => {
let new_state = [...prevState.allBlocks]
new_state.push(blockData)
return { allBlocks: new_state }
})
}
render () {
this.resetValues()
return (
<div className='myBody'>
{this.state.allBlocks.map((blockData, index) => {
this.numberOfBlock++
return (
<CreateBlockStructure
x_axis={blockData.x_axis}
y_axis={blockData.y_axis}
positions={blockData.positions}
key={index}
id={index}
run={blockData.active}
shouldRun={this.shouldRun}
inActivate={this.inActivate}
addBlockStructure={this.addBlockStructure}
/>
)
})}
<button onClick={this.moveLeft}>Move left</button>
<button onClick={this.moveLeft}>Move Right</button>
</div>
)
}
You can checkout the full code for this project
The moveLeft() works properly outside the Keyevent function but when i call it with any other event handler it infinitly calls the setState() method
e.g the MoveLeft button
the moveBlockLeft() is just a simple func in another component
moveBlockLeft(by){
this.setState((prevState)=> {
return {x_axis: prevState.x_axis - by}
})
}
Please i've really spent a lot of time debugging this bug, i have no idea what is going on
Thanks in advance
you are trying to set a value of state variable while update value of another state variable which is why it is ending up into an infinite loop.
Basically in your moveLeft function while setting state you are called moveBlockLeft which is again setting state.
To fix this call moveBlockLeft outised the setState inside your moveLeft.
I am having problems figuring out the correct way how to use useEffect / useLayoutEffect.
I have a component, which waits on a pointerevent (PointerEventTypes.POINTERTAP (comes from babylon). It is within a useLayoutEffect hook. It then fires up pointerClick() to do something.
The problem I am having is that there are 2 dependencies I need to watch: buildings and activeBuilding. How can I achieve that the function pointerClick() does not get triggered two times on the first single click?
I think what is happening is that there is one event (PointerTAP) and the function gets called. Then within the function I am changing activeBuilding which then again triggers a re-render and a second run of the function pointerClick().
If I instead use useEffect (not useLayoutEffect) I have to click twice to set the data because it does change the values after rendering. So it's not what I want.
Here is the example code:
const pointerClick = () => {
if (scene) {
const pickInfo = scene.pick(scene.pointerX, scene.pointerY)
if (buildings.length === 0 && activeBuilding === -1) {
setActiveBuilding(0)
}
if (activeBuilding != -1) {
if (pickInfo?.pickedMesh?.metadata?.measurement?.type === 'handle') {
handleDeletePoint(
pickInfo.pickedMesh.metadata.measurement.id,
activeBuilding
)
} else if (pickInfo?.pickedMesh?.name === 'measureOutline') {
setActiveBuilding(-1)
}
else if (
!pickInfo?.pickedMesh?.metadata?.measurement &&
pickInfo?.pickedPoint
) {
handleAddPoint(pickInfo.pickedPoint, activeBuilding)
}
}
}
}
useLayoutEffect(() => {
if (scene) {
const obs = scene?.onPointerObservable.add((pointerInfoEvent) => {
switch (pointerInfoEvent.type) {
case PointerEventTypes.POINTERTAP:
pointerClick()
// setClicked(false)
break
}
})
return () => {
scene.onPointerObservable.remove(obs)
}
}
}, [canvas, scene, buildings, activeBuilding])
return <Measure />
}
Do I have to get rid of one of the two: buildings or activeBuilding as a dependency (what I think I cannot do since I need both values to be changeable). What am I doing wrong here?
Thank you so much!
I've got some toggles that can be turned on/off. They get on/off state from a parent functional component. When a user toggles the state, I need to update the state in the parent and run a function.
That function uses the state of all the toggles to filter a list of items in state, which then changes the rendered drawing in a graph visualization component.
Currently, they toggle just fine, but the render gets out of sync with the state of the buttons, because the processing function ends up reading in old state.
I tried using useEffect(), but because the function has a lot of dependencies it causes a loop.
I tried coupling useRef() with useState() in a custom hook to read out the current state of at least the newest filter group that was set, but no luck there either.
Any suggestions on how I could restructure my code in a better way altogether, or a potential solution to this current problem?
Gross function that does the filtering:
function filterItems(threshold, items = {}) {
const { values } = kCoreResult;
const { coloredItems } = rgRef.current;
let itemsForUse;
let filteredItems;
if (Object.entries(items).length === 0 && items.constructor === Object) {
itemsForUse = baseItemsRef.current;
} else {
itemsForUse = items;
}
const isWithinThreshold = id => has(values, id) && values[id] >= threshold;
// filter for nodes meeting the kCoreValue criterion plus all links
filteredItems = pickBy(
itemsForUse,
(item, id) => !isNode(item) || isWithinThreshold(id)
);
filteredItems = pickBy(
filteredItems,
item =>
!has(item, 'data.icon_type') || !filterRef.current[item.data.icon_type]
);
setRg(rg => {
rg.filteredItems = leftMerge(filteredItems, coloredItems);
return {
...rg,
};
});
setMenuData(menuData => {
menuData.threshold = threshold;
return {
...menuData,
};
});
}
Function that calls it after button is pressed that also updates button state (button state is passed down from the filter object):
function changeCheckBox(id, checked) {
setFilter(filter => {
filter[id] = !checked;
return {
...filter,
};
});
filterItems(menuData.threshold);
}
It seems calling your filterItems function in the handler is causing the stale state bug, the state update hasn't been reconciled yet. Separate out your functions that update state and "listen" for updates to state to run the filter function.
Here's a demo that should help see the pattern:
export default function App() {
const [filters, setFilters] = useState(filterOptions);
const onChangeHandler = e => {
setFilters({ ...filters, [e.target.name]: e.target.checked });
};
const filterItems = (threshold, items = {}) => {
console.log("Gross function that does the filtering");
console.log("threshold", threshold);
console.log("items", items);
};
useEffect(() => {
filterItems(42, filters);
}, [filters]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
{Object.entries(filters).map(([filter, checked]) => {
return (
<Fragment key={filter}>
<label htmlFor={filter}>{filter}</label>
<input
id={filter}
name={filter}
type="checkbox"
checked={checked}
onChange={onChangeHandler}
/>
</Fragment>
);
})}
</div>
);
}
This works by de-coupling state updates from state side-effects. The handler updates the filters state by always returning a new object with next filter values, and the effect hook triggers on value changes to filters.
I am trying to filter through an array based on checkboxes. When these are ticked, it would show the elements with those attributes (viewable, reported etc). I am having issues with the filter function.
I tried using the forEach method, but that becomes messy with trying to keep the last checkboxes results the same. So I've decided to keep those values the same using state, then to filter the results based on the state. However I am being told 'Cannot read property 'state' of undefined' even though my console log shows the viewable and reported values as 1s or 0s. Not sure what's going wrong but would appreciate a solution to the problem, especially if I'm on the wrong track!
handleCheck = (e) => {
if (e.target.checked) {
this.setState ({
[e.target.name]: '1'
})
} else {
this.setState ({
[e.target.name]: '0'
})
}
console.log(this.state);
this.filterPosts();
}
filterPosts = () => {
this.setState ({
parentArray: this.props.parentResults
})
var filterArray = this.state.parentArray.filter(function(element) {
if (this.state.viewable===1 && this.state.reported===1) {
return element.viewable===1 && element.reportCount>0
} else if (this.state.viewable===1 && this.state.reported===0) {
return element.viewable===1 && element.reportCount===0
} else if (this.state.viewable===0 && this.state.reported===1) {
return element.viewable===0 && element.reportCount>0
} else if (this.state.viewable===0 && this.state.reported===0) {
return element.viewable===0 && element.reportCount===0
}
});
console.log(filterArray);
}
The parentArray comes from the props, and definitely works as I've been able to use forEach methods. I then alter state using checkboxes, and am trying to use this to modify the filter function.
Just want the posts that are viewable or reported to appear and disappear as you check the boxes. Cheers!
The reason you are getting the error is that setState works asynchronously. So when you try to filter your array it is still not initialized.
You should either initialize your parentArray in constructor as
constructor(props){
super(props);
this.state = {
parentArray: this.props.parentResults,}
}
or either just filter your this.props.parentResults in your filter posts function like
filterPosts = () => {
var filterArray = this.props.parentResults.filter(function(element) {
if (this.state.viewable===1 && this.state.reported===1) {
return element.viewable===1 && element.reportCount>0
} else if (this.state.viewable===1 && this.state.reported===0) {
return element.viewable===1 && element.reportCount===0
} else if (this.state.viewable===0 && this.state.reported===1) {
return element.viewable===0 && element.reportCount>0
} else if (this.state.viewable===0 && this.state.reported===0) {
return element.viewable===0 && element.reportCount===0
}
});
console.log(filterArray);
}
I am having an array of objects which is a state object that corresponds to a user account. Just for a validation purpose if any one of the object property within array is empty, I am setting a validation property of state object to false. Since I am looping using for loop, the setState is not setting data.
this.state = {
accounts: [{firstname:"",lastname:"",age:""}],
validated: true
};
onAdd = () => {
let { accounts,validated } = this.state;
for(let i=0; i< accounts.length; i++){
if(accounts[i].firstname === '' || accounts[i].age === '')
{
this.setState({validated: false},() => {}); // value is not getting set
break;
}
}
if(validated)
{
// some other operation
this.setState({validated: true},() => {});
}
}
render(){
let { validated } = this.state;
return(
{validated ? <span>Please fill in the details</span> : null}
//based on the validated flag displaying the error message
)
}
Try to have a separate check that goes through the array in the state and depending if the data is missing or not you can have the temporary variable be either true or false. Then based on the temp variable you can set the state accordingly.
Let me know if this works:
onAdd = () => {
let { accounts,validated } = this.state;
let tempValidate = true; //assume validate is true
for(let i=0; i< accounts.length; i++){
if(accounts[i].firstname === '' || accounts[i].age === '')
{
//this loop will check for the data validation,
//if it fails then you set the tempValidate to false and move on
tempValidate = false
break;
}
}
// Set the state accordingly
if(tempValidate)
{
this.setState({validated: true},() => {});
} else {
this.setState({validated: false},() => {});
}
}
There are multiple issues in your code. First of all this.setState is asynchronous and therefore you can not say when it really is executed.
So if you call
this.setState({validated: false},() => {});
you can not rely that after that this.state.validated is false immediatly.
So after you call this.setState to make validated == false and then check:
if(this.state.validated)
{
// some other operation
this.setState({validated: true},() => {});
}
It might be either true or false.
But in your code you're extracting validated (what you're using in the if-condition) at the beginning of your method. That means validated won't change when this.state changes.
You might want to use the callback you already added (() => {}) to perform some other action after the state changed or just use a normal valiable instead of something related to the state.
Like:
tmp = true;
for-loop {
if(should not validate) {
tmp = false;
this.setState({validated: false});
break;
}
if(tmp) {
this.setState({validated: true},() => {});
}
setState is async function. So it won't work in loops.
From the docs
setState() does not immediately mutate this.state but creates a pending state transition. Accessing this.state after calling this method can potentially return the existing value. There is no guarantee of synchronous operation of calls to setState and calls may be batched for performance gains.
Also refer this.