Child component doesn't rerender but parent component does rerender. How to make child component rerender? - javascript

Parent component does rerender upon receiving new props but its child component doesn't rerender. Child components only render for the first time and never rerender nor receive props from the redux store
I'm getting updated data from redux store in Parent component but not in the child components. Child components only receive data from redux store when they render for the first time
My Parent Component Home.js
Object seaFCLJSON look like this
const seaFCLJSON ={"rates": {"sort":"faster", "someOther": "someOtherValues"}};
when the redux store gets updated, seaFCLJSON looks like this
const seaFCLJSON ={"rates": {"sort":"cheaper","someOther": "someOtherValues"}};
class Home extends Component {
state = {
seaFCLJSON: {}
};
componentDidMount = () => {
this.setState({ seaFCLJSON: this.props.seaFCLJSON });
};
componentWillReceiveProps = nextProps => {
if (this.state.seaFCLJSON !== nextProps.seaFCLJSON) {
this.setState({ seaFCLJSON: nextProps.seaFCLJSON });
}
};
render() {
const { seaFCLJSON } = this.props;
return (
<>
{!isEmpty(seaFCLJSON) && seaFCLJSON.rates && seaFCLJSON.rates.fcl ? (
<FCLContainer fclJSON={seaFCLJSON} />
) : null} //it never rerenders upon getting updated data from redux store
<h5>{JSON.stringify(seaFCLJSON.rates && seaFCLJSON.rates.sort)}</h5> //it rerenders everytime upon getting updated data from redux store
</>
);
}
}
const mapStateToProps = state => {
return {
seaFCLJSON: state.route.seaFCLJSON
};
};
export default connect(
mapStateToProps,
actions
)(Home);
isEmpty.js
export const isEmpty = obj => {
return Object.entries(obj).length === 0 && obj.constructor === Object;
};
My Child Component FCLContainer.js
class FCLContainer extends Component {
state = {
seaFCLJSON: {}
};
componentDidMount = () => {
this.setState({ seaFCLJSON: this.props.seaFCLJSON });
};
componentWillReceiveProps = nextProps => {
console.log("outside state value: ", this.state.seaFCLJSON);
if (this.state.seaFCLJSON !== nextProps.seaFCLJSON) {
this.setState({ seaFCLJSON: nextProps.seaFCLJSON });
console.log("inside state value: ", this.state.seaFCLJSON);
}
};
render() {
const { seaFCLJSON } = this.state;
console.log("rendering .. parent props: ", this.props.fclJSON);
console.log("rendering .. redux store props: ", this.props.seaFCLJSON);
return (
<>
<div className="home-result-container">
<div>
<h5>This child component never rerenders :(</h5>
</div>
</div>
</>
);
}
}
const mapStateToProps = state => {
return {
seaFCLJSON: state.route.seaFCLJSON
};
};
export default connect(
mapStateToProps,
actions
)(FCLContainer);
I don't know whether there are problems in Parent component or problems in the child component. componentWillReceiveProps gets invoked in the parent component but not in the child component. Please ignore any missing semi-colon or braces because I have omitted some unnecessary codes.
Edit 1: I just duplicated value from props to state just for debugging purposes.
I will appreciate your help. Thank you.
Edit 2: I was directly changing an object in redux actions. That's the reason CWRP was not getting fired. It was the problem. For more check out my answer below.

componentWillReceiveProps will be deprecated in react 17, use componentDidUpdate instead, which is invoked immediately after updating occurs
Try something like this:
componentDidUpdate(prevProps, prevState) {
if (this.prevProps.seaFCLJSON !== this.props.seaFCLJSON) {
this.setState({ seaFCLJSON: this.props.seaFCLJSON });
}
};

At the first place it is absolutely meaningless to duplicate value from props to state, what is the meaning of it? Totally pointless, just keep it in props
About your issue - most probably this condition doesnt match, thats why child component doesnt trigger
!isEmpty(seaFCLJSON) && seaFCLJSON.rates && seaFCLJSON.rates.fcl
check it in debugger

As far as I can see, your problem is that you pass the following to your child component:
<FCLContainer fclJSON={seaFCLJSON} />
But you assume that you receive a prop called 'seaFCLJSON':
componentDidMount = () => {
this.setState({ seaFCLJSON: this.props.seaFCLJSON });
};
You should change your code to:
<FCLContainer seaFCLJSON={seaFCLJSON} />
Apart from that, as #Paul McLoughlin already mentioned, you should use the prop directly instead of adding it to your state.

I found the issue I was directly mutating the object in actions. I only knew state should not be directly mutated in class or inside reducer. I changed the actions where I was directly changing an object and then saving it in redux store via dispatch and, then I received the updated props in CWRP. This really took me a lot of times to figure out. This kind of issue is hard to find out at least for me. I guess I get this from https://github.com/uberVU/react-guide/issues/17
A lesson I learned: Never directly mutate an Object
I changed this
//FCL sort by faster
export const sortByFasterFCLJSON = () => async (dispatch, getState) => {
let seaFCLJSON = getState().route.seaFCLJSON;
if (!seaFCLJSON.rates) return;
seaFCLJSON.rates.fcl = _.orderBy(
seaFCLJSON.rates.fcl,
["transit_time"],
["asc"]
);
seaFCLJSON.rates.sort = "Faster"; //this is the main culprit
dispatch({ type: SET_SEA_FCL_JSON, payload: seaFCLJSON });
};
to this
//FCL sort by faster
export const sortByFasterFCLJSON = () => async (dispatch, getState) => {
let seaFCLJSON = getState().route.seaFCLJSON;
if (!seaFCLJSON.rates) return;
seaFCLJSON.rates.fcl = _.orderBy(
seaFCLJSON.rates.fcl,
["transit_time"],
["asc"]
);
// seaFCLJSON.rates.sort = "Faster"; //this was the main culprit, got lost
seaFCLJSON = {
...seaFCLJSON,
rates: { ...seaFCLJSON.rates, sort: "Faster" }
};
dispatch({ type: SET_SEA_FCL_JSON, payload: seaFCLJSON });
};
the power of not mutating data
Side note: Redux Troubleshooting

Related

How to update state of component in componentDidUpdate() without being stuck in an infinite re render?

I have a component with a componentDidMount() method that calls a method called getData() which gets the initial data and sets the initial state of the component.
class LogsSettings extends React.Component {
constructor(props) {
super(props);
this.settingsUrls = [
"/ui/settings/logging"
];
this.state = {
configSettings: {},
formSchema: formSchema
};
this.configSettings = {};
this.selected = "general";
}
getData = (url, selectedSetting) => {
fetch(url)
.then((response) => {
if (response.status !== 200) {
console.log('Looks like there was a problem. Status Code: ' +
response.status);
return;
}
response.json().then((response) => {
//pass formschema here
console.log(selectedSetting);
let newFormSchema = this.setNonDefaultValues(response.data, formSchema.subsections);
Object.assign(this.configSettings, response.data);
this.setState({
configSettings : this.configSettings,
formSchema: newFormSchema
});
});
}
)
.catch((err) => {
console.log('Fetch Error :-S', err);
});
};
componentDidMount() {
this.settingsUrls.map((settingUrl) => {
this.getData(settingUrl, this.selected)
})
}
componentDidUpdate() {
this.settingsUrls.map((settingUrl) => {
this.getData(settingUrl, this.props.selectedSetting)
})
}
render() {
return (
<div className="card-wrapper">
<h2>{formSchema.label.toUpperCase()}</h2>
{
formSchema.subsections.map((subSection) => {
return (
<>
<h3>{subSection['description']}</h3>
<div style={{marginBottom: '10px'}}></div>
{
subSection['input_fields'].map((inputField) => {
return buildForm(inputField, this.handleChange)
})
}
<hr></hr>
</>
)
})
}
<button className="button button-primary">Save Changes</button>
</div>
)
}
}
The selectedSetting parameter that gets passed to the getData() method in this component will change however and when this changes, I need to change the state of the component and get new data specific to the changed selectedSetting parameter.
The new selectedSetting is passed into the component as a prop. The problem is that I can't pass the new selectedSetting parameter to my getData method to update the state of the component as it gets caught in an infinite loop.
How do I go about passing the new selectedSetting to the getData() method without getting caught in an infinite loop? Is this even possible? If not, what is the best approach I should take?
note the selectedSetting parameter isn't used in the getData() function yet but will be and it will be used to get data from an API call and a new form schema which will then lead to the ConfigSettings and formSchema states being changed
If you look closely on the lifecycle of your component, after mount, you'll fetch then update the component. This will trigger the componentDidUpdate lifecycle method which will do the same thing, causing the infinite loop. You need to have a flag that checks whether this.props.selected changed. If it didn't, don't fetch the data else fetch as normal. In the update method, you have access to the previous props. (You may also do this in componentShouldUpdate method, but it'll be just outright risky)
componentDidUpdate(prevProps) {
if( prevProps.selectedSetting !== this.props.selectedSetting ){
this.settingsUrls.map((settingUrl) => {
this.getData(settingUrl, this.props.selectedSetting)
})
}
}
also just a heads up, I noticed that your didMount method, uses a default of "general" as the selected setting, since you want to be using this.props.selectedSetting might be better if it was the one being used instead and just set default props to "general".

Best approach for using same component for editing and adding data. Mixing component state with redux store

I'm building web app in React with Redux. It is simple device manager. I'm using the same component for adding and updating device in database. I'm not sure, if my approach is correct. Here you can find parts of my solution:
UPDATE MODE:
In componentDidMount I'm checking, if deviceId was passed in url (edit mode). If so, I'm calling redux action to get retrieve data from database. I'm using connect function, so when response arrives, It will be mapped to component props.
Here is my mapStateToProps (probably I should map only specific property but it does not matter in this case)
const mapStateToProps = state => ({
...state
})
and componentDidMount:
componentDidMount() {
const deviceId = this.props.match.params.deviceId;
if (deviceId) {
this.props.getDevice(deviceId);
this.setState({ editMode: true });
}
}
Next, componentWillReceiveProps will be fired and I will be able to call setState in order to populate inputs in my form.
componentWillReceiveProps(nextProps) {
if (nextProps.devices.item) {
this.setState({
id: nextProps.devices.item.id,
name: nextProps.devices.item.name,
description: nextProps.devices.item.description
});
}
}
ADD MODE:
Add mode is even simpler - I'm just calling setState on each input change.
handleChange = name => event => {
this.setState({
[name]: event.target.value,
});
};
That's how my inputs looks:
<TextField
onChange={this.handleChange('description')}
label="Description"
className={classes.textField}
value={this.state.description}
/>
I don't like this approach because I have to call setState() after receiving data from backend. I'm also using componentWillReceiveProps which is bad practice.
Are there any better approaches? I can use for example only redux store instead of component state (but I don't need inputs data in redux store). Maybe I can use React ref field and get rid of component state?
Additional question - should I really call setState on each input onChange?
To avoid using componentWillReceiveProps, and because you are using redux, you can do:
class YourComponent extends React.Component {
state = {
// ...
description: undefined,
};
static getDerivedStateFromProps(nextProps, prevState) {
if (prevState.description === undefined && nextProps.description) {
return { description: nextProps.description };
}
return null;
}
componentDidMount() {
const deviceId = this.props.match.params.deviceId;
if (deviceId) {
this.props.getDevice(deviceId);
this.setState({ editMode: true });
}
}
handleChange = name => event => {
this.setState({
[name]: event.target.value,
});
};
// ...
render() {
let { description } = this.state;
description = description || ''; // use this value in your `TextField`.
// ...
return (
<TextField
onChange={this.handleChange('description')}
label="Description"
className={classes.textField}
value={description}
/>
);
}
}
const mapStateToProps = (state) => {
let props = { ...state };
const { devices } = state;
if (devices && devices.item) {
props = {
...props,
id: devices.item.id,
name: devices.item.name,
description: devices.item.description,
};
}
return props;
};
export default connect(
mapStateToProps,
)(YourComponent);
You can then access id, name, and description thought this.props instead of this.state. It works because mapStateToProps will be evaluated every time you update the redux store. Also, you will be able to access description through this.state and leave your TextField as is. You can read more about getDerivedStateFromProps here.
As for your second question, calling setState every time the input changes is totally fine; that's what's called a controlled component, and the react team (nor me) encourage its use. See here.

ReactJS calling function twice inside child component fails to set parent state twice

I'm having an issue where I want to save the data from a particular fieldset with the default values on componentDidMount().
The data saving happens in the parent component, after it is sent up from the child component. However, as React's setState() is asynchronous, it is only saving data from one of the fields. I have outlined a skeleton version of my problem below. Any ideas how I can fix this?
// Parent Component
class Form extends Component {
super(props);
this.manageData = this.manageData.bind(this);
this.state = {
formData: {}
}
}
manageData(data) {
var newObj = {
[data.name]: data.value
}
var currentState = this.state.formData;
var newState = Object.assign({}, currentState, newObj);
this.setState({
formData: newState, // This only sets ONE of the fields from ChildComponent because React delays the setting of state.
)};
render() {
return (
<ChildComponent formValidate={this.manageData} />
)
}
// Child Component
class ChildComponent extends Component {
componentDidMount() {
const fieldA = {
name: 'Phone Number',
value: '123456678'
},
fieldB = {
name: 'Email Address',
value: 'john#example.com'
}
this.props.formValidate(fieldA);
this.props.formValidate(fieldB)
}
render() {
/// Things happen here.
}
}
You're already answering you're own question. React handles state asynchronously and as such you need to make sure you use the current component's state when setState is invoked. Thankfully the team behind React is well-aware of this and have provided an overload for the setState method. I would modify your manageData call to the following:
manageData(data) {
this.setState(prevState => {
const nextState = Object.assign({}, prevState);
nextState.formData[data.name] = data.value;
return nextState;
});
}
This overload for the setState takes a function whose first parameter is the component's current state at the time that the setState method is invoked. Here is the link where they begin discussing this form of the setState method.
https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous
Change manageData to this
manageData(data) {
const newObj = {
[data.name]: data.value
};
this.setState(prevState => ({
formData: {
...prevState.formData,
...newObj
}
}));
}

How to update redux store for many components at the same time on click of a single button?

I have four stateful react components in a single page, they get updated simultaneously on the click of a single button.
I now want to use redux store to save the state of all the components.
I wrote react-redux connect for the first component and it works fine.
Then followed the same logic for the second component, but the store is not getting updated along with the first one.
How can I save the states of all the components at same time to store?
I think you can use this kind of structure.
The main page in which you are using the four stateful components should look like this.
class App extends React.Component {
render() {
const { commonState, updateCommonStateHandler } = this.props;
return (
<div>
<Component1 commonState={commonState} updateCommonStateHandler={updateCommonStateHandler} />
<Component2 commonState={commonState} updateCommonStateHandler={updateCommonStateHandler} />
<Component3 commonState={commonState} updateCommonStateHandler={updateCommonStateHandler} />
<Component4 commonState={commonState} updateCommonStateHandler={updateCommonStateHandler} />
</div>
);
}
}
const mapStateToProps = state => {
return {
commonState: state.commonState
};
};
const mapDispatchToProps = dispatch => {
return {
updateCommonStateHandler: change => {
dispatch(() => ({
type: 'UPDATE_COMMON_STATE',
change
}));
}
};
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
With the reducer
const updateCommonStateReducer = (state = {}, action) => {
const newState = extend({}, state);
if(action.type === 'UPDATE_COMMON_STATE') {
newState.commonState = newState.commonState || {};
extend(newState.commonState, action.change || {});
}
return newState;
};
So if you want to update the state from any child component, you should call the updateCommonStateHandler, which will dispatch an action to update the state.
On state update react will re-render all the components with new commonState.
PS: This is just a sample code explaining the situation, not the solution and it's
written in ES6

React Component not re rendering as expected [duplicate]

This question already has answers here:
Reactjs - Setting State from props using setState in child component
(2 answers)
Closed 5 years ago.
So as I understand, a component will re-render when there has been a change in props and componentWillMount shall run before re-rendering. At the moment my constructor and componentWillMount run as expected, but then the question prop changes which I need to update the user score state, but this change in question prop doesn't trigger the constructor or componentWillMount. As I shouldn't update the state inside the render function (the only place so far that I have been able to get access to the updated question prop), how can I make react recognise this change in it's props and then update the state? Hope that's understandable.
Here is my container
class FullTimeScoreContainer extends Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = {
userHomeScore: 1,
userAwayScore: 1
}
}
componentWillMount() {
getFTSAnswerStatus(this.props.question).then(foundScores => {
if ( foundScores.userHomeScore ) {
this.setState({
userHomeScore: foundScores.userHomeScore,
userAwayScore: foundScores.userAwayScore
});
}
})
}
render() {
const { option, question, questionIndex, user, configs, renderAs, showNextQuestionAfterFTS, total} = this.props;
// check if question is active or not
let ComponentClass;
if ( question[0].active ) {
ComponentClass = FullTimeScoreActive;
} else {
ComponentClass = FullTimeScoreLocked;
}
const changeScoreState = (team, val) => {
switch (team) {
case "home":
this.setState( (prevState) => ({ userHomeScore: prevState.userHomeScore + val }) );
break;
case "away":
this.setState( (prevState) => ({ userAwayScore: prevState.userAwayScore + val }) );
break;
default:
throw new Error("unrecognised team to change score state")
}
}
const onClickCallback = () => {
const p = this.props;
const s = this.state;
p.showNextQuestionAfterFTS();
p.recordFullTimeScoreAnswer(s.userHomeScore, s.userAwayScore, p.question, p.questionIndex, p.user, p.configs)
}
return (
<ComponentClass
imgSrc={imgSrc}
user={user}
answerStatus={answerStatus}
question={question}
onClickCallback={onClickCallback}
questionIndex={questionIndex}
total={total}
configs={configs}
userScores={this.state}
changeScoreState={changeScoreState}
/>
)
}
}
const mapStateToProps = state => {
return {
configs: state.configs,
user: state.user
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({ recordFullTimeScoreAnswer, showNextQuestionAfterFTS }, dispatch);
};
export default connect(mapStateToProps, mapDispatchToProps)(FullTimeScoreContainer);
export { FullTimeScoreContainer }
componentWillMount will only run before the first render. It doesn't get run before every render. So even if your state and props update, componentWillMount will not get called again.
The constructor function is the same as well.
You might be looking for componentWillReceiveProps (see docs). This lifecycle event is called when a mounted component is about to receive new props. You can update your state in this lifecycle event.
Note that componentWillReceiveProps only works on mounted components. Therefore, it will not get called the first time your component receives its' initial props.
A side note: Per the docs, you also don't want to introduce any side-effects or subscriptions in componentWillMount. Do that in componentDidMount instead.
I would like add a comment, but I don't have enough reputation...
a component will re-render when there has been a change in props
As I understand, you can't change the props, so component re-render on state changes.

Categories

Resources