I'm using React functional components and here are my codes:
Parent component function:
const calculateAPR = async (val) => {
setIsAprLoading(true);
try {
if (val.addr !== "" && val.date !== null) {
const totalStaking = await someEP.getSomeData(val.addr);
val.staked = totalStaking;
setResData((prevState) => {
return ({
...prevState,
aprRes: val
})
})
setRenderApr(true);
setIsAprLoading(false);
}
else {
setRenderApr(false);
alert(Constants.ADDR_N_DATE_ERR);
}
}
catch (err) {
setRenderApr(false);
console.log(err);
alert(Constants.ADDR_NO_DATA);
}
finally {
setIsAprLoading(false);
}
}
...
return (
...
<QueryAprField text={Constants.CALC_APR_HEADER} onFunction={calculateAPR} isLoading={isAprLoading} />
<CalculateAprField resData={resData.aprRes} onRender={renderApr} />
...
)
Child component 1:
function QueryAprField(props) {
...
const handleQuery = () => {
const verify = verifyDelegatorAddress();
if (verify) {
props.onFunction(queryValue);
}
else {
alert(Constants.ENTER_VALID_DEL_ADDR);
}
}
...handles taking in user inputs and passing it to parent component...
}
Child component 2:
function CalculateAprField(props) {
const aprRes = props.resData;
...
const renderCard = () => {
if (renderData == true) {
const aprInput = setAprInputs(aprRes);
const { staked } = extractAprInput(aprInput);
const apr = parseFloat(calculateAPR(staked, accrued, withdrawn, numOfDays).toFixed(5));
if (isNaN(apr)) {
//How to reset aprRes and ensure that its not using old values
return alert(Constants.APR_AUTO_ERR)
}
return (
<Paper elevation={4}>
...some html and css...
</Paper>
)
}
}
return (
<Box>
{renderCard()}
</Box>
)
I'm trying to enable a situation where, after calculateAPR in the parent component is executed, some data will be passed to child component 2; and in child component 2 in the renderCard function, if the variable apr in child component 2 is NaN then an alert will be triggered. However, the problem I'm facing now is that after the alert is triggered, and when I put in new values and execute calculateAPR again, child component 2 seems to use the old values first before using the new values that are passed down from the parent component. So in other words, I get the alert first and then it uses the new values that are being passed down.
How can I enable the aprRes variable in child component 2, to reset its value after the alert is thrown? So that the alert is not thrown twice?
There is no need to reset the value. component rerenders only on props change.
I see the problem could be with this line. const aprInput = setAprInputs(aprRes); React setState doesn't return anything. Please change it to below and try once.
setAprInputs(aprRes);
(or)
if aprInput is not used anywhere else better extract from aprRes only.
const { staked } = extractAprInput(aprRes);
Incase if setAprInputs is not a setState rather a user-defined function, ensure it is a synchronous function and console.log after the call.
hope this gives you some insight to debug.
Related
I've tried to find a solution to this, but nothing seems to be working. What I'm trying to do is create a TreeView with a checkbox. When you select an item in the checkbox it appends a list, when you uncheck it, remove it from the list. This all works, but the problem I have when I collapse and expand a TreeItem, I lose the checked state. I tried solving this by checking my selected list but whenever the useEffect function runs, the child component doesn't have the correct parent state list.
I have the following parent component. This is for a form similar to this (https://www.youtube.com/watch?v=HuJDKp-9HHc)
export const Parent = () => {
const [data,setData] = useState({
name: "",
dataList : [],
// some other states
})
const handleListChange = (newObj) => {
//newObj : { field1 :"somestring",field2:"someotherString" }
setDataList(data => ({
...data,
dataList: data.actionData.concat(newObj)
}));
return (
{steps.current === 0 && <FirstPage //setting props}
....
{step.current == 3 && <TreeForm dataList={data.dataList} updateList={handleListChange}/>
)
}
The Tree component is a Material UI TreeView but customized to include a checkbox
Each Node is dynamically loaded from an API call due to the size of the data that is being passed back and forth. (The roots are loaded, then depending on which node you select, the child nodes are loaded at that time) .
My Tree class is
export default function Tree(props) {
useEffect(() => {
// call backend server to get roots
setRoots(resp)
})
return (
<TreeView >
Object.keys(root).map(key => (
<CustomTreeNode key={root.key} dataList={props.dataList} updateList={props.updateList}
)))}
</TreeView>
)
CustomTreeNode is defined as
export const CustomTreeNode = (props) => {
const [checked,setChecked] = useState(false)
const [childNodes,setChildNodes] = useState([])
async function handleExpand() {
//get children of current node from backend server
childList = []
for( var item in resp) {
childList.push(<CustomTreeNode dataList={props.dataList} updateList={props.updateList} />)
}
setChildNodes(childList)
}
const handleCheckboxClick () => {
if(!checked){
props.updateList(obj)
}
else{
//remove from list
}
setChecked(!checked)
}
// THIS IS THE ISSUE, props.dataList is NOT the updated list. This will work fine
// if I go to the next page/previous page and return here, because then it has the correct dataList.
useEffect(() => {
console.log("Tree Node Updating")
var isInList = props.dataList.find(function (el) {
return el.field === label
}) !== undefined;
if (isInList) {
setChecked(true);
} else {
setChecked(false)
}
}, [props.dataList])
return ( <TreeItem > {label} </TreeItem> )
}
You put props.data in the useEffect dependency array and not props.dataList so it does not update when props.dataList changes.
Edit: Your checked state is a state variable of the CustomTreeNode class. When a Tree is destroyed, that state variable is destroyed. You need to store your checked state in a higher component that is not destroyed, perhaps as a list of checked booleans.
I have a react component which passes user input value to a props function. The parent function just appends those inputs to an object. However, when an older value is modified, all the newer values are removed. Please refer to the screenshots.
This is the parent. ExamArea.js
import McqQuestion from './McqQuestion'
import React, { useState, useEffect } from 'react';
import './ExamArea.css'
function ExamArea(props) {
const[currentQuesID, setCurrentQuesID] = useState(2);
const[mcqQuestionList, setmcqQuestionList] = useState([<McqQuestion returnfunc={returnFromMcqQuestion} id={1}/>]);
const[ques, setQues] = useState({});
function returnFromMcqQuestion(quesID, thisQuestion) {
var temp = {...ques};
temp["ques"+quesID] = thisQuestion;
console.log(temp);
setQues(temp);
}
function generateMCQ(questionid) {
return (<McqQuestion returnfunc={returnFromMcqQuestion} id={questionid}/>)
}
function addAnotherQuestion() {
setmcqQuestionList(mcqQuestionList.concat(generateMCQ(currentQuesID)));
setCurrentQuesID(currentQuesID+1);
}
return (
<div className="ExamArea">
{mcqQuestionList}
<button onClick={()=>addAnotherQuestion()} class="add_another_question_button">+ Add Another Question</button>
</div>
);
}
export default ExamArea;
This is the child.
import './McqQuestion.css'
import React, { useState, useEffect } from 'react';
import { Paper, TextField } from '#material-ui/core';
import InputBase from '#material-ui/core/InputBase';
/*
This is the component that lets the maker create the question, and then stores the question to packedQuestion.
packedQuestipn is in a format which can be directly sent to the API to be uploaded to the database.
A basic question has question_text, question.title
Props passed:
props.id = The Question ID.
props.returnfunc = The function that gets called with packedQuestion and props.id when everything is done.
props.returnfunc(props.id, packedQuestion) is the thing that is called.
*/
function McqQuestion(props) {
const [packedQuestion, setPackedQuestion] = useState({});
useEffect(()=> props.returnfunc(props.id, packedQuestion));
/*These two variables store a local copy of packedQuestion. These variables are first updated with the information from
onChange (or a variation of it), and then packedQuestion is set to an instance of this. */
let local_question_mcq = {};
let local_answerChoices_mcq = {};
function fillUpQuestionWithDefault(){
function addOption(character, value) {
local_answerChoices_mcq[character] = value;
local_question_mcq["answer_choices"] = local_answerChoices_mcq;
}
function addQuestion(title, value){
if(title){
local_question_mcq['title'] = value;
}
else {
local_question_mcq['question_text'] = value;
}
}
addQuestion(true, "Question "+props.id);
addQuestion(false, "");
addOption("a", "");
addOption("b", "");
addOption("c", "");
addOption("d", "");
local_question_mcq['title'] = "Question " + props.id;
local_question_mcq['id'] = props.id;
setPackedQuestion(local_question_mcq);
}
useEffect(() =>fillUpQuestionWithDefault(), []);
function optionOnInputFunc(character, value) {
local_question_mcq = {...packedQuestion};
local_answerChoices_mcq = {...local_question_mcq["answer_choices"]};
local_answerChoices_mcq[character] = value;
local_question_mcq["answer_choices"] = local_answerChoices_mcq;
setPackedQuestion(local_question_mcq);
}
function questionOnInputFunc(title, value) {
if(title){
local_question_mcq = {...packedQuestion};
local_question_mcq['title'] = value;
setPackedQuestion(local_question_mcq);
}
else {
local_question_mcq = {...packedQuestion};
local_question_mcq['question_text'] = value;
setPackedQuestion(local_question_mcq);
}
}
function mcqChoiceGeneratingFunc() {
return (
<div class = "Opt">
<TextField onChange = {e => optionOnInputFunc('a', e.target.value)} label="Option A" variant="filled" multiline rowsMax={4}/>
<TextField onChange = {e => optionOnInputFunc('b', e.target.value)} label="Option B" variant="filled" multiline rowsMax={4}/>
<TextField onChange = {e => optionOnInputFunc('c', e.target.value)} label="Option C" variant="filled" multiline rowsMax={4}/>
<TextField onChange = {e => optionOnInputFunc('d', e.target.value)} label="Option D" variant="filled" multiline rowsMax={4}/>
</div>
);
}
return (
<Paper class="Question">
<form class="Question-form">
<a class = "editpencil">✎</a>
<InputBase class = "questionedit"
onChange = {e => questionOnInputFunc(true, e.target.value)}
defaultValue={"Question "+props.id}
inputProps = {{"maxlength": 40}}/>
<div class="question-text">
<TextField onChange = {e => questionOnInputFunc(false, e.target.value)} variant="outlined" fullWidth="true" label="Type your question"></TextField>
</div>
{mcqChoiceGeneratingFunc()}
</form>
</Paper>
);
}
export default McqQuestion;
The behavior I am describing can be seen in these screenshots.
The first two screenshots are expected. Two new questions were added and their respective objects were in the console log.
Expected Behavior at the start of the state
Expected behavior when two new questions were added
When question 1 was edited while questions 2 and 3 were there, the objects for question 3 disappeared.
Why is this happening and how do I fix this?
Issues
ExamArea
Storing react components in state is a React anti-pattern and sure-fire way to get yourself some stale state enclosures.
Store just the data in state and render the UI from it.
Any time you are updating react state that depends on the previous state (i.e. appending an element to an array, incrementing a count/id, etc...) you don't use a functional state update.
Use a functional state update to correctly update from any previous state versus state from the previous render cycle.
McqQuestion
Once I resolved your issues in ExamArea I was a bit thrown off by the usage of local_question_mcq and local_answerChoices_mcq. At first glance they appeared to be "state" that wasn't part of component state.
Limit the scope of utility variables such as local_question_mcq and local_answerChoices_mcq
Similar issues with the functional updates, but coupled to the overscoped local_question_mcq and local_answerChoices_mcq.
Use a functional state update to directly update packedQuestion in the onChange handlers.
Solution
ExamArea
Store only data in component state.
Map state to UI in render function.
Use functional state update to map previous state to next state. Use the question ID to match the question that needs to be updated and also shallow copy it.
Pass returnFromMcqQuestion as prop directly (not stored in state either).
Code:
function ExamArea(props) {
const [currentQuesID, setCurrentQuesID] = useState(2);
const [mcqQuestionList, setmcqQuestionList] = useState([{ id: 1 }]); // <-- store data only
function returnFromMcqQuestion(quesID, thisQuestion) {
setmcqQuestionList((mcqQuestionList) => // <-- functional state update
mcqQuestionList.map((question) =>
question.id === quesID // <-- shallow copy matching question
? {
...question,
...thisQuestion
}
: question
)
);
}
function generateMCQ(questionid) {
return {
id: questionid
};
}
function addAnotherQuestion() {
setmcqQuestionList((mcqQuestionList) => // <-- functional state update
mcqQuestionList.concat(generateMCQ(currentQuesID))
);
setCurrentQuesID((c) => c + 1); // <-- functional state update
}
return (
<div className="ExamArea">
{mcqQuestionList.map(({ id }) => (
<McqQuestion
key={id}
returnfunc={returnFromMcqQuestion} // <-- pass callback directly
id={id}
/>
))}
<button
onClick={addAnotherQuestion}
className="add_another_question_button"
>
+ Add Another Question
</button>
</div>
);
}
McqQuestion
Use functional state update to map previous state to next state.
Limit the scope of local_question_mcq and local_answerChoices_mcq, move them into fillUpQuestionWithDefault and declare them const.
Make code more DRY where possible.
Fix class vs className and other various React warnings.
Code:
function McqQuestion(props) {
const [packedQuestion, setPackedQuestion] = useState({});
useEffect(() => {
props.returnfunc(props.id, packedQuestion); // <-- update state in parent
}, [packedQuestion]);
function fillUpQuestionWithDefault() {
/*These two variables store a local copy of packedQuestion. These variables are first updated with the information from
onChange (or a variation of it), and then packedQuestion is set to an instance of this. */
const local_question_mcq = { // <-- provide initial values, then override
id: props.id,
title: `Question ${props.id}`,
};
const local_answerChoices_mcq = {};
function addOption(character, value = '') {
local_answerChoices_mcq[character] = value;
local_question_mcq["answer_choices"] = local_answerChoices_mcq;
}
function addQuestion(title, value) {
local_question_mcq[title ? "title" : "question_text"] = value; // <-- DRY
}
addQuestion(true, "Question " + props.id);
addQuestion(false, "");
['a', 'b', 'c', 'd'].forEach(c => addOption(c, '')); // <-- DRY
setPackedQuestion(local_question_mcq);
}
useEffect(() => {
fillUpQuestionWithDefault();
}, []);
function optionOnInputFunc(character, value) {
setPackedQuestion((question) => ({ // <-- functional state update
...question,
answer_choices: {
...question.answer_choices,
[character]: value
}
}));
}
function questionOnInputFunc(title, value) {
setPackedQuestion((question) => ({ // <-- functional state update
...question,
[title ? 'title' : 'question_text']: value
}));
}
function mcqChoiceGeneratingFunc() {
return (
<div className="Opt">
...
</div>
);
}
return (
<Paper className="Question">
...
</Paper>
);
}
When you are calling this function from child component then ques take the value of initial state in hook that is {}. Now you are adding key quesID in temp and updating the state. So it will be an expected behavior.
function returnFromMcqQuestion(prevQues, quesID, thisQuestion) {
var temp = {...prevQues};
temp["ques"+quesID] = thisQuestion;
setQues(prevQues);
}
So you need something like this.
<McqQuestion ques={ques} returnfunc={returnFromMcqQuestion} id={questionid}/>)
useEffect(()=> props.returnfunc(props.ques, props.id, packedQuestion));
I'm passing renderProps function in the props. i want to wrap it with useCallback, so the optimised child component will no re-render on the function creation.
when wrapping the function with useCallback i get this error:
Invalid hook call. Hooks can only be called inside of the body of a
function component. This could happen for one of the following
reasons:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app
none of the above applies to my situation.
renderCell = React.useCallback((
{
events,
popperPlacement,
popperStyle,
time
}
) => {
const { localeToggle } = this.state;
const { weekStarter, isTimeShown } = this.props;
const eventsListPopperStyle = utils.isWeekFirst(time, weekStarter) ||
utils.isWeekSecond(time, weekStarter) ? { left: '-17% !important' } : { left: '17% !important' };
return (
<MonthlyCell
events={events}
isTimeShown={isTimeShown}
popperPlacement={popperPlacement}
popperStyle={popperStyle}
time={time}
eventsListPopperStyle={eventsListPopperStyle}
/>
)
}, [])
Because hooks doesn't work inside class components, the error was thrown.
I managed to find a work around by providing the second parameter for the React.memo. in the function i provided, i compare the prevProps and nextProps, and when the prop is a function i disregard it and return true.
it might not work for everyone because sometime the function do change, but for situations when it's not, it's ok.
const equalizers = {
object: (prevProp, nextProp) => JSON.stringify(prevProp) === JSON.stringify(nextProp),
function: () => true, // disregarding function type props
string: (prevProp, nextProp) => prevProp === nextProp,
boolean: (prevProp, nextProp) => prevProp === nextProp,
number: (prevProp, nextProp) => prevProp === nextProp,
}
export const areEqualProps = (prevProps, nextProps) => {
for (const prop in prevProps) {
const prevValue = prevProps[prop];
const nextValue = nextProps[prop];
if (!equalizers[typeof prevValue](prevValue, nextValue)) { return false; }
}
return true
}
export default React.memo(MyComponent, areEqualProps)
I am mapping through an array, which returns JSX Components for each of the items in the array. During runtime I want to pass down values. If they match the value of the individual items, their individual component gets modified.
I am trying to find a way to achieve this without rerendering all components, which currently happens because the props change
I have tried using shouldComponentUpdate in a class component, but it seems this way I can only compare prevState and prevProps with the corresponding changes. I have further considered useMemo in the Map function, which didnt work, because it was nested inside the map function.
const toParent=[1,2,4,5]
Parent Component:
function parent({ toParent }) {
const [myNumbers] = useState([1,2,3,4, ..., 1000]);
return (
<div>
{myNumbers.map((number, index) => (
<Child toChild = { toParent } number = { number }
index= { index } key = { number }/>
))}
</div>
)
}
Child Component:
function Child({toChild, number, index}){
const [result, setResult] = useState(() => { return number*index }
useEffect(()=> {
if (toChild.includes(number)) {
let offset = 10
setResult((prev)=> { return { prev+offset }})
}
}, [toChild])
return (
<div style={{width: result}}> Generic Div </div> )
}
The solution to my problem was using the React.memo HOC and comparing the properties to one another and exporting it as React.memo(Child, propsAreEqual).
Performance
This way other methods like findElementbyId (not recommended in any case) and shouldComponentUpdate to target specific items in a map function can be avoided.
Performance is quite good, too. Using this method cut down the rendering time from 40ms every 250ms to about 2 ms.
Implementation
In Child Component:
function Child(){...}
function propsAreEqual(prev, next) {
//returning false will update component, note here that nextKey.number never changes.
//It is only constantly passed by props
return !next.toChild.includes(next.number)
}
export default React.memo(Child, propsAreEqual);
or alternatively, if other statements should be checked as well:
function Child(){...}
function propsAreEqual(prev, next) {
if (next.toChild.includes(next.number)) { return false }
else if ( next.anotherProperty === next.someStaticProperty ) { return false }
else { return true }
}
export default React.memo(Key, propsAreEqual);
Hi I have a parent component, having two child component as follows, Child1 has draggable div element, which on drag gives value out to Parent component which then I have to pass to Child2 and utilise, but before utilising it in Child2 I have to make a dozens of calculations.
const Parent = () => {
const [dragValue, setDragValue] = useState(0);
const dragHanlder = (dragVal) => {
setDragValue(dragVal);
};
return (
<Child1 mouseDrag={dragHanlder} />
<Child2 dragValue={dragValue}/>
);
}
class Child2 extends React.Component {
state = {
afterCalculationsVal: 0
};
componentDidUpdate = () => {
const { dragValue } = this.props;
const someFinalval = val //this val takes dragValue applies checks, calculations and conditions and finally gives out some value
this.setState({afterCalculationsVal: someFinalval });
};
render() {
const { afterCalculationsVal } = this.state;
return (
<Child3 afterCalculationsVal={afterCalculationsVal} >
);
}
}
Problem is I'm getting maximum depth reached issue, because I'm setting state for drag which is continuous. Is there any way I can overcome this. I cannot use the 'dragValue' coming in props in Child2 directly, the calculations on the props value is mandatory, and I have to set state after that.
The problem is that when the component updates it is going into componentDidUpdate then setting state causing another update. Setting an if condition checking drag value should fix your problem.
componentDidUpdate = (prevProps, prevState) => {
const { dragValue } = this.props;
if(prevState.dragValue !== prevState.dragValue){
// Will only re-render when dragValue changes
const someFinalval = val
this.setState({afterCalculationsVal: someFinalval });
}
};
Never use this.setState in the componentdidupdate without checking the value that you change are really changed. Infact this trigger an infinit loop
componentDidUpdate = () => {
const { dragValue } = this.props;
const someFinalval = val //this val takes dragValue applies checks, calculations and conditions and finally gives out some value
if(/** your check *//)
this.setState({afterCalculationsVal: someFinalval });
};