So, i am trying to pass data from child component to parent component. I was able to do this through using props and is able to print the data using console.log. However, when i try to setState in the parent component using this data from child component. I am stuck in infinite loop of updating state which caused error "Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops."
My code is of below
Parent component
import React, { Component } from 'react'
import TagsPaper from './tagsPaper'
import CurrentTagPaper from './currentTagPaper'
import ApiCall from './Backend/apiCall';
export default class Tags extends Component {
constructor(props) {
super(props);
this.state = {
tags: ["apple"],
toUpdate: true,
};
}
componentDidMount() {
}
getTags = (e) => {
const array = e;
console.log(array)
this.setTags(array)
return array
}
setTags(e) {
this.setState({
tags: e
})
}
render() {
return (
<div >
<p>
Get recommendations for articles that matches your
interests when you follow more tags:
</p>
<b>
You are following
</b>
<TagsPaper></TagsPaper>
<b>
From this article
</b>
{/* pass Tag array name here*/}
<CurrentTagPaper tagArray={this.state.tags} />
<ApiCall getTags={this.getTags} />
</div>
)
}
}
Child component
import React, { Component } from 'react';
import '../App.css';
export default class ApiCall extends Component {
constructor(props) {
super(props);
this.state = {
response: '',
post: '',
responseToPost: '',
tags: this.props.tags
};
this.sendTags = this.sendTags.bind(this);
}
componentDidMount() {
this.getTagApi()
}
getTagApi(){
this.callApi()
.then(res => this.setState({ response: res.express }))
.catch(err => console.log(err));
}
callApi = async () => {
const response = await fetch('http://localhost:5000/api/scrap');
const body = await response.json();
if (response.status !== 200) throw Error(body.message);
return body;
};
sendTags = (value) => {
this.props.getTags(value);
}
handleSubmit = async e => {
e.preventDefault();
const response = await fetch('http://localhost:5000/api/forms', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ post: this.state.post }),
});
const body = await response.text();
this.setState({ responseToPost: body });
};
render() {
let tagArray = [...this.state.response];
this.sendTags(tagArray);
return (
<div className="App">
{/* {tagArray.map(e => <p key={e}>{e}</p>)} */}
<form onSubmit={this.handleSubmit}>
<p>
<strong>Post to Server:</strong>
</p>
<input
type="text"
value={this.state.post}
onChange={e => this.setState({ post: e.target.value })}
/>
<button type="submit">Submit</button>
</form>
<p>{this.state.responseToPost}</p>
</div>
);
}
}
When the parent changes state, it renders the child. When the child renders, it updates the parent state. There would be a few clever ways to fix this, but your ultimate problem is that copying data into both a parent and child's state isn't very idiomatic.
Generally, you should "hoist" any state used by both parent and child to only exist in the parent, and this "hoisting" should happen right off the API call.
In ApiCall, you could switch to
getTagApi(){
this.callApi()
.then(res => this.sendTags(res.express))
.catch(err => console.log(err));
}
In your parent Tags component, you would need to pass the parents tag state back down.
<ApiCall getTags={this.getTags} tags={this.state.tags} />
Your ApiTags would then just use the parent's tags.
let tagArray = this.props.tags;
You also would not want to copy this.props.tags into this.state during construction.
React expects that there be one source-of-truth for all data. If a higher-level component needs the data as well as a lower-level component, you should always hoist the data, as in, pass it up whenever it changes, and pass it back down as a prop on every render.
Related
So I'm a beginner with react and I was wondering how to re-render the child after setting the state in the parent (from the child). Here's a code sample. I have a function that calls a GET request using Axios and when I press the button in the child component ideally it will update the state in the parent and also re-render the child but it only does the former.
Parent:
class Parent extends Component {
constructor(props) {
super(props);
this.state = {
data: []
}
}
fetchData = () => {
axios
.get(url)
.then(res => this.setState({data: res.data}))
}
Render() {
return (<Child data={this.state.data} fetchData={this.fecthData}/>)
}
// ...
Child:
class Child extends Component {
// ...
render() {
const { data, fetchData } = this.props
// render data
return <button onClick={fetchData}>Change data then fetch</button>
}
}
Also, are you supposed to make a local state in the Child and set it as a copy of the Parent's state or just passing it down as a prop is okay?
Your parent component holds the data and the child uses it. It seems to me you're doing it the right way. Here is a fully working example:
Codesandbox
class Parent extends Component {
constructor(props) {
super(props);
this.state = {
data: []
};
this.updateData = this.updateData.bind(this);
}
async fetchData() {
const response = await fetch("https://jsonplaceholder.typicode.com/posts");
return response.json();
}
updateData() {
this.setState({ data: [] }) // Creates a flicker, just so you see it does refresh the child
this.fetchData().then((res) => this.setState({ data: res }));
}
render() {
return <Child data={this.state.data} onAction={this.updateData} />;
}
}
Note I renamed your child prop fetchData into onAction (I don't know what's the name of the action that triggers a refresh, could be onRefresh). It's always best to see components props with separation between data attributes and event attributes.
Even standard components have it this way: <input value={user.firstname} onChange={doSomething} />. So, better to prefix events by on, then the parent decides what to do with it. It's not the child's concern.
class Child extends Component {
render() {
const { data, onAction } = this.props;
return (
<>
<button onClick={onAction}>Change data then fetch</button>
{data.map((item) => (
<div key={item.id}>
{item.id} - {item.title}
</div>
))}
</>
);
}
}
After a post I would like to reload my table to be able to display the data after the post. Now the question arises how to get my "DataProvider" to render again?
I would do this as a function call in "FormOPCConnect". But I don't know how to start. I already tried to use the "props" of the "DataProvider", but I can't figure out how to render the new table.
Enclosed my source code.
TableOPCConnections.js
import React from "react";
import PropTypes from "prop-types";
import key from "weak-key";
import Table from 'react-bootstrap/Table'
const OPCVarTable = ({ data }) =>
!data.length ? (
<p>Nothing to show</p>
) : (
<div>
<h2 className="subtitle">
Showing <strong>{data.length}</strong> OPC Variables
</h2>
<Table striped bordered hover>
<thead>
<tr>
{Object.entries(data[0]).map(el => <th key={key(el)}>{el[0]}</th>)}
</tr>
</thead>
<tbody>
{data.map(el => (
<tr key={el.id}>
{Object.entries(el).map(el => <td key={key(el)}>{el[1]}</td>)}
</tr>
))}
</tbody>
</Table>
</div>
);
OPCVarTable.propTypes = {
data: PropTypes.array.isRequired
};
export default OPCVarTable;
DataProvider.js
import React, { Component } from "react";
import PropTypes from "prop-types";
class DataProvider extends Component {
static propTypes = {
endpoint: PropTypes.string.isRequired,
render: PropTypes.func.isRequired
};
state = {
data: [],
loaded: false,
placeholder: "Loading..."
};
componentDidMount() {
fetch(this.props.endpoint)
.then(response => {
if (response.status !== 200) {
return this.setState({ placeholder: "Something went wrong" });
}
return response.json();
})
.then(data => this.setState({ data: data, loaded: true }));
}
render() {
const { data, loaded, placeholder } = this.state;
return loaded ? this.props.render(data) : <p>{placeholder}</p>;
}
}
export default DataProvider;
FormOPCConnect.js
(Here I'd like to refresh the state of the DataProvider)
After the fetch method I would like to render the table again as long as the post to the database was successful.
import React, { Component, useState } from "react";
import PropTypes from "prop-types";
import Button from "react-bootstrap/Button";
import Form from 'react-bootstrap/Form'
import DataProvider from "./DataProvider";
import csrftoken from './csrftoken';
class FormOPCConnect extends Component {
constructor(props) {
super(props);
this.state = {
validated: false
};
}
static propTypes = {
endpoint: PropTypes.string.isRequired
};
state = {
ip_address: "",
port: "",
namespace_name: "",
root_name: "",
owner: ""
};
handleChange = e => {
this.setState({ [e.target.name]: e.target.value });
};
handleSubmit = event => {
const form = event.currentTarget;
if (form.checkValidity() === false) {
event.preventDefault();
event.stopPropagation();
}
event.preventDefault();
const { ip_address, port, namespace_name, root_name, owner } = this.state;
const opcConn= { ip_address, port, namespace_name, root_name, owner };
const conf = {
method: "post",
body: JSON.stringify(opcConn),
headers: new Headers({ "Content-Type": "application/json", "X-CSRFTOKEN": csrftoken })
}
fetch(this.props.endpoint, conf).then(response => console.log(response));
//>>
//if response is valid -> refresh the Dataprovider and the table...
//<<
this.setState({ validated: this.state.validated = true })
};
App.js
const App = () => (
<React.Fragment>
<Container>
<Row>
<Col> <NavBarTop fixed="top" /> </Col>
</Row>
<Row>
<Col> <DataProvider endpoint="opcconnection/"
render={data => <OPCVarTable data={data} />} /></Col>
<Col><FormOPCConnect endpoint="opcconnection/" /></Col>
</Row>
</Container>
</React.Fragment>
);
const wrapper = document.getElementById("app");
wrapper ? ReactDOM.render(<App />, wrapper) : null;
I'm new to React, so please forgive my mistakes. :D
Finally it looks like this.
OPCConnection_Image
Your code currently contains 2 problems that need to be fixed in order to update your table when you post data.
1)
Your DataProvider does actually rerender on props change. The problem here is that your logic to fetch your data is in componentDidMount. componentDidMount only triggers the first time the component mounts and doesn't trigger on rerender.
If you want your data to fetch everytime the component rerenders you could place your fetch functionality in your render method of DataProvider.
To rerender a component all you have to do is update its props or it's state.
2) You want your DataProvider to update when your FormOPCConnect has finished some logic.
The thing with React is. You can only pass variables from parents to children. You can't directly communicate from sibling to sibling or from child to parent.
In your App your DataProvider is a siblign of FormOPCConnect, they are next to eachother.
<App> // App can communicate with every component inside it.
<DataProvider /> // This component can't communicate with the component next to it.
<FormOPCConnect />
</App>
The easiest thing to do here would be to either render DataProvider inside FormOPCConnect and update DataProvider's props directly.
Or if that is not possible, keep a state in App which checks if your logic in FormOPCConnect has finished.
class App extends Component {
constructor(props) {
super(props);
this.state = { boolean: false }; //state is remembered when a component updates.
}
flipBoolean() { //this function updates the state and rerenders App when called.
this.setState(
boolean: boolean!
);
};
render {
return (
<Fragment>
<DataProvider />
<FormOPCConnect flipBoolean={this.flipBoolean} />
</Fragment>
)
}
}
Pass a function to FormOPCConnect which updates the state of App. When you want your DataProvider to rerender you simply call that flipBoolean function in FormOPCConnect. This will update the state in App. Which will trigger App to rerender. Which will in it's turn rerender it's children DataProvider andFormOPCConnect`.
(This variable doesn't need to be a boolean, you can do here whatever you want. This boolean is just an example).
I am using ComponentDidMount to call data from my database and render page when data is ready. However, i have noticed the speed of my application has reduced when navigating since i have to wait for the data.
This is happening when i have large data in the database i am retrieving. My question is, is there any way of optimizing this, or i just have to render page before data loads ?
Component.JS
componentDidMount()
{
this.fetchAllItems();
}
fetchAllItems(){
return this.fetchPost().then(([response,json]) => {
console.log('here now ',response);
console.log(localStorage.getItem('user_token'))
if(response.status === 200)
{
}
})
}
fetchPost(){
const URL = 'http://localhost:8000/api/';
return fetch(URL, {method:'GET',headers:new Headers ({
'Accept': 'application/json',
'Content-Type': 'application/json',
})})
.then(response => Promise.all([response, response.json()]));
}
Try to use axios to make call to API asynchronously, after it's done, just update your response data to state. No need to wait your page is finished loading or not, react will render by following changes of state value.
import React from 'react';
import axios from 'axios';
export default class MovieList extends React.Component {
state = {
movies: []
}
componentDidMount() {
axios.get(`http://localhost/movies`)
.then(res => {
const movies = res.data;
this.setState({ movies: movies });
})
}
render() {
const {
movies
} = this.state;
return (
<div>
<ul>
{ movies.map(movie => <li>{movie.name}</li>)}
</ul>
</div>
)
}
}
Have you tried the shouldComponentUpdate lifecycle method? This method accepts nextProps (new or upcoming props) and nextState (new or upcoming State) parameters. You can compare your next props and state (state preferably in your case) to determine if your component should re-render or not. Fewer re-renders equals to better speed and optimization. that means your pages will load faster. The shouldComponentUpdate method returns a boolean to determine if a page should re-render or not. Read more here. Also, Here's an example:
class App extends React.Component {
constructor() {
super();
this.state = {
value: true,
countOfClicks: 0
};
this.pickRandom = this.pickRandom.bind(this);
}
pickRandom() {
this.setState({
value: Math.random() > 0.5, // randomly picks true or false
countOfClicks: this.state.countOfClicks + 1
});
}
// comment out the below to re-render on every click
shouldComponentUpdate(nextProps, nextState) {
return this.state.value != nextState.value;
}
render() {
return (
<div>
shouldComponentUpdate demo
<p><b>{this.state.value.toString()}</b></p>
<p>Count of clicks: <b>{this.state.countOfClicks}</b></p>
<button onClick={this.pickRandom}>
Click to randomly select: true or false
</button>
</div>
);
}
}
ReactDOM.render(
<App />,
document.getElementById('app')
);
In your case all the optimization must be done in the backend.
But if there is something that can be done in React is using Should Component Update as previous comment mentioned.
I need to make a new api request to fetch data for a given dataId.
this value lives in the Context.
import { MyContext } from './Context'
class MyComponent extends Component {
constructor(props) {
super(props)
this.state = {
dataId: this.context.state.dataId // tried setting state first but didn´t work.
}
this.details = this.details.bind(this)
}
details() {
fetch('https://api.mydomain.com/' + this.context.state.dataId)
.then(response => response.json())
.then(data => this.setState({ data: data }));
}
componentDidMount() {
this.details()
}
render() {
return(
<MyContext.Consumer>
{(context) => (
<div>data: {JSON.stringify(data)} dataId: {context.state.dataId}</div>
)}
</MyContext.Consumer>
)
}
}
MyComponent.contextType = MyContext;
export default MyComponent
from others components I can set new values like
this.context.setDataId(1)
and this will show up correctly but the problem is that is not making a new fetch to get new data for the dataId that changed in the Context.
not sure what´s the correct lifecycle method I can use to detect changes in the context and make a new call to this.details()
I didn´t add the Context code here because it works fine. but if you need to see it please let me know.
In react, you must use life cycle hooks to inspect data such as props or context, to know if the state needs to update for your component. The most common life cycle hook for this purpose is componentDidUpdate(). it gives you the ability to decide whether or not your component needs to update state/rerender based on changes in props that caused the component to update. the following should work for your use case:
import { MyContext } from './Context'
class MyComponent extends Component {
state = {
data:[],
dataId:null
}
details = () => {
// we only want to update if dataId actually changed.
if(this.context.state.dataId !== this.state.dataId){
this.setState({dataId:this.context.state.dataId});
fetch('https://api.mydomain.com/' + this.context.state.dataId)
.then(response => response.json())
.then(data => this.setState({ data: data }));
}
}
componentDidMount() {
this.details()
}
componentDidUpdate() {
this.details();
}
render() {
return(
<MyContext.Consumer>
{(context) => (
<div>data: {JSON.stringify(this.state.data)} dataId: {context.state.dataId}</div>
)}
</MyContext.Consumer>
)
}
}
MyComponent.contextType = MyContext;
export default MyComponent;
I wrote the dropdown component that passes a selected value back to parent via callback function. From there I would like to simply render the selected value below the dropdown. Instead I have rendered previous state. I have no idea why that works like that, could someone explain me my app's behaviour and maybe give a hint how to fix it? I don't even know where to look for the answers.
index.js
import React, { Component } from 'react';
import './App.css';
import { Dropdown } from './components/dropdown'
class App extends Component {
state = {
response: "",
currA: ""
};
componentDidMount() {
this.callApi()
.then(res => this.setState({ response: res.express }))
.catch(err => console.log(err));
}
callApi = async () => {
const response = await fetch('/main');
const body = await response.json();
if (response.status !== 200) throw Error(body.message);
return body;
};
calculateRate = (currA) => {
this.setState({currA: currA});
}
render() {
return (
<div className="App">
<div>
<Dropdown callbackFromParent={this.calculateRate}/>
</div>
<p>
{this.state.currA}
</p>
</div>
);
}
}
export default App;
dropdown.js
import React from 'react';
export class Dropdown extends React.Component {
constructor(props){
super(props);
this.state = {
list: [],
selected: ""
};
}
componentDidMount(){
fetch('https://api.fixer.io/latest')
.then(response => response.json())
.then(myJson => {
this.setState({ list: Object.keys(myJson.rates) });
});
}
change(event) {
this.setState({ selected: event.target.value });
this.props.callbackFromParent(this.state.selected);
}
render(){
var selectCurr = (curr) =>
<select
onChange={this.change.bind(this)}
value={this.state.currA}
>
{(this.state.list).map(x => <option>{x}</option>)}
</select>;
return (
<div>
{selectCurr()}
</div>
);
}
}
Since your setState() is not a synchronous call, it might be that your callback is firing before the state of your dropdown is actually modified. You could try using the callback on setState...
change(event) {
this.setState({
selected: event.target.value
}, () => {this.props.callbackFromParent(event.target.value)});
;
}
...Or if your parent component is the only thing that cares about the selected value (my guess from your snip), you don't need to update the dropdown state at all.
change(event) {
this.props.callbackFromParent(event.target.value;)
}
Good luck!
Documentation:
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied. If you need to set the state based on the previous state, read about the updater argument below.