I have two components, App and Child. My goal is that App passes an id down to Child and when Child receives it as props, I want Child to perform an Axios/Fetch request and then update itself with the result data. I will accept any answer that provides me with an example App/Child source code that completes my example or gives my insight if this is actually possible what I'm trying to implement.
I am a new to React.
// App.js
import React, { Component } from 'react';
import Child from './Child.js';
import './App.css';
class App extends Component {
state = {
id: 0
};
handleClick() {
this.setState({
id: this.state.id + 1
});
}
render() {
return (
<div>
<Child id={this.state.id} />
<div>
<button onClick={this.handleClick.bind(this)}>Pass new id down to Child component</button>
</div>
</div>
);
}
}
export default App;
// Child.js
import React, { Component } from 'react';
import axios from 'axios';
class Child extends Component {
state = {
data: null,
id: 0
};
loadData(q, cb) {
axios.get('http://localhost:3000/foo?q='+this.state.id)
.then(result => {
// ?
// this.setState would retrigger update and create an infinite updating loop
})
.catch(error => {
console.log('error: ' + error);
});
}
shouldComponentUpdate(nextProps, prevState) {
console.log('shouldComponentUpdate: nextProps = ' + JSON.stringify(nextProps) + ', prevState = ' + JSON.stringify(prevState));
// ??
}
getSnapshotBeforeUpdate(nextProps, prevState) {
console.log('getSnapshotBeforeUpdate: nextProps = ' + JSON.stringify(nextProps) + ', prevState = ' + JSON.stringify(prevState));
// ??
}
static getDerivedStateFromProps(nextProps, prevState) {
console.log('getDerivedStateFromProps: nextProps = ' + JSON.stringify(nextProps) + ', prevState = ' + JSON.stringify(prevState));
return {
id: nextProps.id
};
// ??
}
componentDidUpdate() {
console.log('componentDidUpdate');
}
render() {
return (
<div>
<div>data = {this.state.data}</div>
</div>
);
}
}
export default Child;
You should call check prev Id with current id to avoid recursive update. You just need to make sure you only derive state from props if your props have changed,
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.id !== prevState.id) {
return {
id: nextProps.id
};
}
return null;
}
Let me know if your issue still persists
This could be one way to go about your problem,
Firstly, you'll have to update all the state data inside your getDerivedStateFromProps
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.id !== prevState.id) {
// Returning this object is equivalent to setting state using this.setState
return {
id: nextProps.id
data: loadData(nextProps.id) // Something like this
};
}
return null; // Indicates no state change
}
As for the api call inside loadData method you can make use of the aync / await to return the data from api call
async loadData(id) {
try {
// Make your api call here
let response = await axios.get('http://localhost:3000/foo?q='+id);
return await response.json();
} catch(err) {
// catches errors both in axios.get and response.json
// Make sure to not update state in case of any error
alert(err);
return null;
}
}
Note:
This is not the complete solution as you might want to not update the state in the case where your api call in loadData catches any error.
Also, since you are using api calls, you might want to limit the times pass props to the child cause they all will trigger different api calls and you will run into unpredictable states. Try debouncing.
Related
Good day,
I'm trying to fetch a new data once a new props has changed on my route.
Here's my sample routing:
<Route path={'/students/:selectedPage'} exact component={Students} />
Basically, I have a method that fetches my data. So I'm invoking it in my componentDidMount() Here are my sample codes:
componentDidMount(): void {
this.getData()
}
getData(selectedPage){
axios.get(`http://localhost:1343/students/${selectedPage}`).then(response=>{
this.setState({
students: response.data
})
}).catch(error=>console.log(error)
}
For my componentDidUpdate method which trigger if there's new changes in the param of my url.
componentDidUpdate(): void {
if (this.props.match.params.selectedPage !== prevProps.selectedPage){
this.getData(this.props.match.params.selectedPage)
}
}
Unfortunately, it causes to have infinite request from the web api. Which makes the table laggy. I tried to create a state with a name, hasLoaded but it gives me an error message that says, infinite loop has detected.
Any advice?
Compare your Props in ComponentWillReceiveProps like this,
shouldComponentUpdate: function(nextProps, nextState) {
return nextProps.selectedPage != this.props.selectedPage ;
}
componentWillReceiveProps(nextProps) {
if (nextProps.selectedPage !== this.props.selectedPage ) {
//this.setState({ selectedPage : nextProps.selectedPage })
this.getData(nextProps.selectedPage)
}
}
or Do this instead Keep your selectedPage in state
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.selectedPage !== prevState.selectedPage ) {
return { selectedPage : nextProps.selectedPage };
}
else return null;
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.selectedPage !== this.props.selectedPage ) {
//Perform some operation here
//this.setState({ selectedPage : this.props.selectedPage });
this.getData(this.props.selectedPage)
}
}
```
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".
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
I have a component that looks like this:
export default class Class1 extends Component {
render = async () => {
await AsyncStorage.getItem('someValue', (error, someValue) => {
return(
<Class2 prop1={someValue}/>
)
}
}
}
What's happening here is that I need to render Class1 based on the value of someValue that is returned from AsyncStorage. The problem is, you can't make the render() method async because async functions return a promise, and render() needs to return a React component.
Does anyone know how I can do this?
Thanks!
For this kind of tasks, you would put the value in your state. And based on the state, you will render the class as required.
In your componentDidMount of Class1, write:
componentDidMount() {
AsyncStorage.getItem('value').then((val) => {
this.setState({ value: val });
})
}
Then in your Class1 add a method which will generate the class based on state value:
createClass() {
// do something with the value
if (this.state.value === somevalue) {
return (
<Class2 />
)
}
return null;
}
And in your render method of Class1, type:
render() {
return (
{ this.createClass() }
)
}
You can set it to state, for example:
componentDidMount() {
AsyncStorage.getItem('someValue', (e, someValue) => {
this.setState({someValue})
}
}
Then you can use someValue from state in your render.
Currently, in addition to the async render issue, since you're already passing in a callback to AsyncStorage.getItem(), I'm not sure what using async/await would do.
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
}
}));
}