2

I am new to React. I have to implement a button that adds few fields(text boxes, select, checkbox) on the page on every click of the button. I have achieved this by referring example http://jsfiddle.net/rpd32h1g/. I have 3 classes 1. Parent class where I have written the logic to populate the subSectionList which defines the components/fields that need to be added on the click of a button. 2. Child component(AddAccount.js) which receives the param subSectionList from the parent and passes it to the array of sub child Account.js. This array is created on click of the button and on each click the array is updated. 3. Sub child component (Account.js) which receives the param subSectionList and iterate it to show the required components. Problem: The sub child Account.js is not being re-rendered if the value of subSectionList is changed in the parent. Below is the code for reference:

Calling below function(findApplicableFields) onChange of field added using the button(this methid exists in parent class).

const findApplicableFields = (questions, questionName, optionName, currentQuestion) => {
 const newSubSectionList = questions.map(question => {
            if (question.name === questionName) {
                return { ...question, children };
            }
            return { ...question };
        });
 subSectionList[questionName] = newSubSectionList;
}

Below is the code snippet of the child:

import React, { useState } from 'react';
import PropTypes from 'prop-types';
import Account from './Account';

export default function AddAccount({
    buttonText,
    type = 'button',
    buttonClass,
    questions,
    checkoutJourney,
    handleBlur,
    isValid,
    question,
    onChange,
    subSectionList,
    ...rest
}) {
    const [addAccount, setAddAccount] = useState(true);
    const [accountArray, setAccountArray] = useState(
        checkoutJourney['addAccountArray'] == null ? [] : checkoutJourney['addAccountArray']
    );
    let i = 0;

    const _onClick = e => {
        e.preventDefault();
        setAddAccount(true);
        setAccountArray([
            <Account
                key={i}
                questions={questions}
                checkoutJourney={checkoutJourney}
                handleBlur={handleBlur}
                onChange={onChange}
                isValid={isValid}
                questionName={question.name}
                subSectionList={subSectionList}
                {...rest}
            />,
            ...accountArray
        ]);
        checkoutJourney['addAccountArray'] = accountArray;
        checkoutJourney['subSectionList'] = subSectionList;
        i++;
    };
    return (
        <>
            {addAccount ? accountArray : null}
            <div className={buttonClass}>
                <button
                    className="button button--dark button--primary"
                    type={type}
                    onClick={e => {
                        _onClick(e);
                    }}
                >
                    {buttonText}
                </button>
            </div>
        </>
    );
}

AddAccount.propTypes = {
    _onClick: PropTypes.func,
    type: PropTypes.string,
    buttonText: PropTypes.string,
    buttonClass: PropTypes.string,
    questions: PropTypes.array,
    onChange: PropTypes.func,
    checkoutJourney: PropTypes.object,
    rest: PropTypes.object,
    handleBlur: PropTypes.func,
    isValid: PropTypes.object,
    question: PropTypes.any,
    subSectionList: PropTypes.object
};

AddAccount.defaultProps = {
    buttonText: '+ Add another account'
};

Below is the code snippet of sub child which needs to be re-rendered:

import React from 'react';
import PropTypes from 'prop-types';
import TextInput from '../TextInput/WS2TextInput';
import Select from '../Select/WS2Select';
import AddressMoreServices from '../AddMoreServices/AddMoreServices';
import Checkbox from '../Checkbox/Checkbox';
const Account = ({ checkoutJourney, handleBlur, isValid, onChange, subSectionList, questionName, ...rest }) => {

    return (
        <>
            {subSectionList[questionName].map((question, i, questions) => {
               //The logic to render the field based on type of question 
 })}
        </>
    );
}

Account.propTypes = {
    questions: PropTypes.array,
    onChange: PropTypes.func,
    checkoutJourney: PropTypes.object,
    rest: PropTypes.object,
    handleBlur: PropTypes.func,
    isValid: PropTypes.object,
    subSectionList: PropTypes.object,
    questionName: PropTypes.string
};

export default Account;


1 Answers1

3

This is happening because you're not changing the subSectionList variable, but instead manipulating its content (adding and removing properties in an object). React will only rerender Components if the variables they're bound to change completely (in other words, they're swapped in memory). React identifies changes by performing a shallow comparison between your old and new state variables. In your case the variable subSectionList is always the same (since you're simply adding properties to it), therefore React won't rerender <AddAccount /> and <Account />.

In order to rerender <AddAccount /> and <Account /> you'll have to recreate the entire subSectionList by making a setState call in your parent component:

const [subSectionList, setSubSectionList] = useState([]);

setSubSectionList([
    ...subSectionList,
    [questionName]: newSubSectionList
]);

This will generate a completely new subSectionList array in memory, making React re-render all components that depend on it.

VulfCompressor
  • 1,390
  • 1
  • 11
  • 26
  • In my case, the keys in subsectionList will remain the same and only the value of the key will change. Can you please help to understand how can I re-render child in this situation? – Somya Srivastava Jan 29 '20 at 22:32
  • @SomyaSrivastava you'll still have to generate a completely new variable, like the example above. React will only re-render a Component if a variable it depends upon is completely swapped in memory. Changing your object values simply isn't enough. – VulfCompressor Jan 29 '20 at 23:31
  • I have set the subsection list completely. But still, the sub child(Account.js) is not being re-rendered whereas the updated subSectionList is getting printed in child i.e. AddAccount.js ```let newSubSectionList = {}; subChildren.map(child => { if (child.apply_to === optionName) { newSubSectionList[child.name] = true; } else { newSubSectionList[child.name] = false; } }); setSubSectionList(newSubSectionList);``` – Somya Srivastava Jan 30 '20 at 23:56
  • I found the cause, the problem is happening because of the array of Account.js. If I remove the array and just render Account.js using a flag that is set to true on click of the button then my sub child is being re-rendered on change of subSecTionList. But the problem with this approach is that I can't add multiple objects of Account on click of Add Account button. Can you please help me with this? – Somya Srivastava Jan 31 '20 at 00:47
  • I'm afraid I can't offer any more help in the comment section as it's not open for extended discussions. `Components` should always re-render if they're bound to a state variable that is changing. I think you should take a good second look at your code and make sure all components are properly bound to your state. – VulfCompressor Jan 31 '20 at 01:35