React Component not re rendering as expected [duplicate] - javascript

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.

Related

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

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

How to prevent container component from re-rendering when child component updates

I've been trying to optimise an app by preventing the container component in the example below from re-rendering every time the child component updates. The app example only has one counter which is updated via a key. I've stripped the example down to just one counter although I'm using multiple counters in the actual app.
It works by dispatching an increment action every second, which updates the redux state. The container maps through each counter (in this case just one) held in state and renders a child component which just displays the count.
I've looked at using PureComponent and shouldComponentUpdate but couldn't get it to stop re-rendering and I'm also not sure if the latter is the best approach. From similiar questions I've gleaned that the problem might be that both the container and child component are referring to the same object in state and therefore are both updated. Sandbox example here: https://codesandbox.io/s/nostalgic-rubin-k1gxy
Container
class CounterContainer extends React.PureComponent {
renderWatches = () => {
//was some map logic here originally hence separate method
const { counters } = this.props;
const counterKeys = Object.keys(counters);
return counterKeys.map(key => {
return <SingleCounter key={key} mapKey={key} />;
});
};
render() {
console.log("CounterContainer rendered");
return <div>{this.renderWatches()}</div>;
}
}
const mapStateToProps = state => {
return {
counters: state.counters
};
};
export default connect(mapStateToProps)(CounterContainer);
Child
export const SingleCounter = props => {
useEffect(() => {
const interval = setInterval(() => {
props.dispatch(actionCreators.increment(props.counter.key));
}, 1000);
return () => clearInterval(interval);
});
console.log("Child rendered");
return <div>{props.counter.counter}</div>;
};
export const mapStateToProps = (state, ownProps) => {
return {
counter: state.counters[ownProps.mapKey]
};
};
export default connect(mapStateToProps)(SingleCounter);
Looking at the console logs, I'd ideally like to see the child component re-render every time the counter is incremented however the container should render just once (or in the actual app, whenever another counter is added)

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
}
}));
}

React render method that depends on asynchronous request and state change

I am trying to learn ReactJS and Redux, and have come across a problem that I cannot seem to get over.
I have a React component, that gets data from an asynchronous request.
export class MyPage extends React.Component {
constructor(props) {
super(props)
this.state = {
enableFeature: false,
}
this.handleEnableFeatureChange = this.handleEnableFeatureChange.bind(this)
}
componentWillMount () {
this.fetchData()
}
fetchData () {
let token = this.props.token
this.props.actions.fetchData(token)
}
handleEnableFeatureChange (event) {
this.setState({ enableFeature: event.target.checked })
}
render () {
if (this.props.isFetching) {
return (
<div>Loading...</div>
)
} else {
return (
<div>
<label>Enable Feature
<input type="checkbox"
className="form-control"
checked={this.props.enableFeature}
onChange={this.handleEnableFeatureChange}
/>
</label>
</div>
)
}
}
}
So, my problem now is that, when I change the state of the checkbox, I want to update the state of my data. However, every time I update the state of my data, the react component method shouldComponentUpdate kicks in, and uses the current props to render the original data.
I would like to see how such cases are handled in general.
Thanks.
Try to do it like the following, i.e.
Use componentWillReceiveProps to assign props.enableFeature to state.enableFeature. From documentation
componentWillReceiveProps() is invoked before a mounted component receives new props. If you need to update the state in response to prop changes (for example, to reset it), you may compare this.props and nextProps and perform state transitions using this.setState() in this method.
Note that React may call this method even if the props have not changed, so make sure to compare the current and next values if you only want to handle changes. This may occur when the parent component causes your component to re-render.
componentWillReceiveProps() is not invoked if you just call this.setState()
Use this state to load the value of checkbox
Manipulate this state (onchange) to update the value of checkbox
Following code can work in your case
export class MyPage extends React.Component {
static propTypes = {
isFetching: React.PropTypes.bool,
enableFeature: React.PropTypes.bool,
token: React.PropTypes.string,
actions: React.PropTypes.shape({
fetchData: React.PropTypes.func
})
};
state = {
enableFeature: false,
};
componentWillMount () {
this.fetchData();
}
/* Assign received prop to state, so that this state can be used in render */
componentWillReceiveProps(nextProps) {
if (this.props.isFetching && !nextProps.isFetching) {
this.state.enableFeature = nextProps.enableFeature;
}
}
fetchData () {
const { token } = this.props;
this.props.actions.fetchData(token)
}
handleEnableFeatureChange = (event) => {
this.setState({ enableFeature: event.target.checked })
};
render () {
return (<div>
{ this.props.isFetching && "Loading..." }
{
!this.props.isFetching && <label>
Enable Feature
<input
type="checkbox"
className="form-control"
checked={this.state.enableFeature}
onChange={this.handleEnableFeatureChange}
/>
</label>
}
</div>);
}
}
Note: The above code was not executed, but should work (babel's stage-0 code)
Change it to checked={this.state.enableFeature}

Categories

Resources