Here I am trying to pass data between sibling components A and B, where I want to re-render component B upon completion of an asynchronous event in component A.
class ParentComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
valueKey: '',
};
}
parentFunction(dataFromChild) {
this.setState({ valueKey: dataFromChild });
}
render() {
const { valueKey } = this.state;
return (
<div>
<componentA
url={url}
...
functionCallFromParent={() => this.parentFunction()}
/>
<componentB valueFromParent={valueKey} />
</div>
);
}
}
class componentA extends React.Component {
constructor(props) {
super(props);
this.state = {
items: null,
};
// asynchronous request made to server which updates the state when response received.
this._getTiles();
}
render() {
const { items } = this.state;
if (items && items.asArray().length > 0) {
functionCallFromParent('Hello From Child1');
return (
<div>
{tiles}
</div>
);
}
return null;
}
}
Issue is, every time componentA calls functionCallFromParent() ParentComponent's render is executed, and it re-renders componentA and componentB and this loop continues. Whereas, I want only componentB to be re-rendered when componentA ask it to.
functionCallFromParent() is put inside render() in ComponentA which means it will get called every time the component is rendered/re-rendered which happens quite a lot! When that happens, the ParentComponent.parentFunction() will be called to set its own state which in turn causes a re-render to itself and its own children and this is how the loop is created. You should move this to the part of the code where you receive the response.
Related
Let's have components A and B.
Component B only shows the parent's state in contentForB in a different format and doesn't manipulate with it. It takes A's state as a prop, applies a function transform(content) and shows it, so whenever A's contentForB changes, the new content get transformed and updated in B.
The problem comes when A wants to use B's transformed content and use it somewhere else. I tried to implemented in a standard way, using state-updating function and passed it from A to B like this:
class A extends React.Component {
constructor(props) {
super(props);
this.state = {
contentForB: "",
transformedContent: ""
};
this.updateTransformedContent = this.updateTransformedContent.bind(this);
}
updateTransformedContent(newContent) {
this.setState = { transformedContent: newContent };
}
render() {
return (
...
<B content={ this.state.contentForB }
updateTransformedContent={ this.updateTransformedContent } />
<ComponentUsingTransformedContent transformedContent = { this.state.transformedContent } />
);
}
}
class B extends React.Component {
constructor(props) {
super(props);
this.transform = this.transform.bind(this);
}
transform(content) {
let newContent = ...;
this.props.updateTransformedContent(newContent);
return newContent;
}
render() {
<Something value={this.transform(this.props.content)} />
}
}
However, when A's state changes, B gets reinitialized, it then changes A's state by calling the updateTransformedContent which again causes B to get reinitialized, thus causing an infinite loop even though the updateTransformedContent changes the state object which isn't directly passed into B.
Any ideas how to deal with such situation properly?
You can use componentDidUpdate lifecycle method.
[codesandbox.io]
https://codesandbox.io/s/cool-leaf-92nt8?file=/src/App.js:457-475
class TestB extends Component {
constructor(props) {
super(props);
this.state = {
transformed: ""
};
this.transform = this.transform.bind(this);
}
componentDidUpdate(prevProps) {
if (this.props.content !== prevProps.content) {
this.transform(this.props.content);
}
}
transform(content) {
const transformed = `<h4>${this.props.content}</h4>`;
this.setState({ transformed: transformed });
this.props.updateTransformedContent(transformed);
return transformed;
}
render() {
return (
<div>
<h4>B Componenet:</h4>
<p>{this.state.transformed}</p>
</div>
);
}
}
I have two components, one parent one child. I am using the fetch method in componentDidMount() callback. Once I do this, I set the state with key items to that data that is pulled from the api. Once I do this it should be able to be console logged in the child component as a prop. However this is not working. What am I doing wrong here?
Parent Component:
import React, {Component} from 'react';
import Map from './maps/Map';
class Main extends Component {
constructor(props){
super(props);
this.state = {
name: "John",
items: []
}
}
componentDidMount() {
fetch('https://hn.algolia.com/api/v1/search?query=')
.then(dat => dat.json())
.then(dat => {
this.setState({
items: dat.hits
})
})
}
render() {
return (
<div>
<Map list={this.state.name} items={this.state.items}></Map>
</div>
)
}
}
export default Main;
Child Component:
import React, {Component} from 'react';
class Map extends Component {
constructor(props) {
super(props);
console.log(props.items)
}
render () {
return (
<h1>{this.props.name}</h1>
)
}
}
export default Map;
First, fetch is asynchronous. So, the fetch statement might be pending by the time you try to console.log the result inside the child constructor.
Putting the console.log inside the render method would work, because the component will be rerendered, if the state items changes.
The constructor for a component only runs one time during a lifecycle. When it does, props.items is undefined because your ajax request is in-flight, so console.log(props.items) doesn't show anything.
If you change your constructor to console.log("constructed");, you'll see one-time output (stack snippets may not show this--look in your browser console). Henceforth, componentDidUpdate() can be used to see the new props that were set when your ajax request finishes.
You could also log the props inside the render method, which will run once before the ajax request resolves and again afterwards when props.items changes.
As a side point, you have <Map list=... but the component tries to render this.props.name, which is undefined.
Also, if you aren't doing anything in the constructor (initializing state or binding functions) as here, you don't need it.
class Map_ /* _ added to avoid name clash */ extends React.Component {
constructor(props) {
super(props);
console.log("constructed");
}
componentDidUpdate(prevProps, prevState) {
const props = JSON.stringify(this.props, null, 2);
console.log("I got new props", props);
}
render() {
return (
<div>
<h1>{this.props.name}</h1>
<pre>
<ul>
{this.props.items.map((e, i) =>
<li key={i}>{JSON.stringify(e, null, 2)}</li>)}
</ul>
</pre>
</div>
);
}
}
class Main extends React.Component {
constructor(props) {
super(props);
this.state = {name: "John", items: []};
}
componentDidMount() {
fetch('https://hn.algolia.com/api/v1/search?query=')
.then(dat => dat.json())
.then(dat => {
this.setState({items: dat.hits})
});
}
render() {
return (
<div>
<Map_
name={this.state.name}
items={this.state.items}
/>
</div>
);
}
}
ReactDOM.createRoot(document.querySelector("#app"))
.render(<Main />);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="app"></div>
The only problem you have is that you are trying to use this.props.name and your Map component props are called list and items, so it will return undefined.
If you log your props in the constructor you will get the initial state of Main because the fetch hasn't returned anything yet. Remember that the constructor only runs once. So you are probably getting an empty array when you log props.items in the constructor because that's what you have in your initial state.
{
name: "John",
items: []
}
If you log the props in your render method you will see your array filled with the data you fetched, as you can see here:
https://codesandbox.io/s/stoic-cache-m7d43
If you don't want to show the component until the data is fetched you can include a boolean property in your state that you set to true once you the fetch returns a response and pass it as a prop to your component. Your component can you use that variable to show, for example, a spinner while you are fetching the data. Here's an example:
https://codesandbox.io/s/reverent-edison-in9w4
import CircularProgress from "#material-ui/core/CircularProgress"
class Main extends Component {
constructor(props) {
super(props);
this.state = {
name: "John",
items: [],
fecthed: false
};
}
componentDidMount() {
fetch("https://hn.algolia.com/api/v1/search?query=")
.then(dat => dat.json())
.then(dat => {
this.setState({
items: dat.hits,
fecthed: true
});
});
}
render() {
return (
<Map
fetched={this.state.fecthed}
list={this.state.name}
items={this.state.items}
/>
);
}
}
class Map extends Component {
render() {
return (
<div>
{this.props.fetched ? (
<div>
<h1>{this.props.list}</h1>
{this.props.items.map((item, indx) => (
<div key={indx}>Author: {item.author}</div>
))}
</div>
) : (
<CircularProgress />
)}
</div>
);
}
}
Hope this helps. Cheers!
I have this code:
export default class FinancesPage extends React.Component {
constructor(props) {
super(props);
this.state = {users: []};
}
componentWillMount() {
firebase.database().ref('Users').orderByChild('transactions').startAt(1).on('value', snap => {
const users = arrayFromObject(snap.val());
this.setState({users: users});
});
}
render() {
return (
<div>
<NumberOfPurchasesComponent users={this.state.users}/>
</div>
)
}
}
And this code:
export default class NumberOfPurchasesComponent extends React.Component {
constructor(props) {
super(props);
this.state = {users: this.props.users};
}
componentWillMount() {
const users = this.state.users;
// Do stuff here
}
render() {
return (
{/*And render stuff here*/}
);
}
}
What's happening right now: The parent element FinancesPage passes an empty array of users to the child NumberOfPurchasesComponent. I need it to pass a new value of the array every time there is an update.
And i want to pass the users from FinancesPage to NumberOfPurchasesComponent, but users data is obtained async. How can I make the NumberOfPurchasesComponent refresh when the variable value is obtained?
Have you tried to use componentWillReceiveProps? I mean something like:
export default class NumberOfPurchasesComponent extends React.Component {
constructor(props) {
super(props);
this.state={users: []}
}
componentWillReceiveProps(nextProps) {
if(nextProps.users && nextProps.users!==this.state.users){
this.setState({
users: nextProps.users
})
}
}
render() {
return (
{/*And render stuff here*/}
);
}
}
This way the component knows when it has to re-render.
The FinancesPage implementation looks good. The problem lies in NumberOfPurchasesComponent in this particular piece of code :
constructor(props) {
super(props);
this.state = {users: this.props.users};
}
Am assuming in the render method of NumberOfPurchasesComponent you are using this.state.users instead of this.props.users.
constructor runs only once. Now as you mentioned data is fetched async, which means NumberOfPurchasesComponent is initially rendered even before the the response is obtained. Hence its constructor method which runs only once sets the users state to []. Even if the props gets updated from FinancesPage, as the render in NumberOfPurchasesComponent uses state, no re-render happens.
Try using this.props.users directly in NumberOfPurchasesComponent render and see if it works.
As per FinancesPage page it is well and good with codebase, but problem is why you are making setstate if there is no any manipulation of user's data as you got from API call.
So without making setState just pass it as direct
render() {
return (
<div>
<NumberOfPurchasesComponent users={this.props.users}/>
</div>
)
}
so whenever the API calls to fetch the response, here update value get in passed to NumberOfPurchasesComponent class.
I am trying to save child-data in the state of the parent, but end up with the endless loop because setState() calls render().
Error message: 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.
Relatively new to React, so I can't seem to word the question when googling solutions. I know why the error is occurring, I just don't know how to get around the issue. Is there a specific method I can use that prevents re-rendering?
Here is the parent:
export class ToDoList extends React.Component {
constructor(props){
super(props);
this.state = {
data: null
}
}
myCallback = (dataFromChild) => {
this.setState({data: dataFromChild.toUpperCase()})
}
render() {
return (
<div>
<ToDoItem callbackFromParent={this.myCallback}/>
</div>
);
}
}
The child:
class ToDoItem extends React.Component {
constructor(props){
super(props);
this.state = {
listInfo: 'Doggos'
}
}
render(){
return(
<h1>{this.props.callbackFromParent(this.state.listInfo)}</h1>
);
}
}
Your code is doing exactly that, an endless loop. When your ToDoItem component renders, it calls callbackFromParent which updates the state of ToDoList, causing ToDoList to re-render, subsequently re-rendering the ToDoItem. Since ToDoItem re-renders, it calls callbackFromParent again and so on...
I'd like to ask why you are trying to render the non-value-returning function of callbackFromParent. It doesn't return anything, so it doesn't make sense why you'd want to render it inside of your <h1> tags.
There is a small problem with the code you shared, that you are calling a function from the render() rather than binding it to some event which is making it go into infinite loop...
Is this what you are trying to achieve?
class ToDoList extends React.Component {
toUpper = (dataFromChild) => {
return dataFromChild.toUpperCase();
}
render() {
return (
<div>
<ToDoItem toUpper={this.toUpper}/>
</div>
);
}
}
class ToDoItem extends React.Component {
constructor(props){
super(props);
this.state = {
listInfo: 'Doggos'
}
}
render(){
return(
<h1>{this.props.toUpper(this.state.listInfo)}</h1>
);
}
}
ReactDOM.render(<ToDoList />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
You are getting an endless loop because of the following reasons:
1) You are calling the parent callback on each render.
2) You are saving the uppercased value in the parent state.
When the parent state gets updated, the child gets re-rendered, meaning that it will call the callback again, which will cause to re-render, which calls the callback again etc...
An alternative solution would be to pass the util function down to the child which can then call it once when it re-renders. Since no state in the parent is being updated, the child will not be re-rendered.
If you're trying to save data on parent but want to display it in child, try this:
Here is the parent:
export class ToDoList extends React.Component {
constructor(props){
super(props);
this.state = {
data: null
}
}
myCallback = (dataFromChild) => {
this.setState({data: dataFromChild})
}
render() {
return (
<div>
<ToDoItem callbackFromParent={this.myCallback} data={this.state.data}/>
</div>
);
}
}
The child:
class ToDoItem extends React.Component {
constructor(props){
super(props);
this.state = {
times: 0
}
// Bind Explained below
this.iBeClicked = this.iBeClicked.bind(this);
}
iBeClicked(){
this.setState({times: ++this.props.data});
this.props.callbackFromParent(this.props.data++);
}
render(){
return(
<div className="wrap">
<h1 onClick="iBeClicked">{this.props.data !== null ? this.props.data: 'Nothing' }</h1>
</div>
);
}
}
You use this.method.bind(this) in order to bind Component's this to React callback's execution inside render.
When clicking the button, the text "second message" and "third message" should be added to the state through these functions:
class Button extends React.Component {
constructor(props) {
super(props);
this.addMessage = this.addMessage.bind(this);
}
addMessage() {
this.props.addMessage('second message'); // This is ignored
this.props.addMessage('third message'); // Only this message is added
}
render() {
return(
<button onClick={this.addMessage}>Add text</button>
)
}
}
But only one of them is firing.
I created a simple snippet that shows the problem.
class Button extends React.Component {
constructor(props) {
super(props);
this.addMessage = this.addMessage.bind(this);
}
addMessage() {
this.props.addMessage('second message'); // This is ignored
this.props.addMessage('third message'); // Only this message is added
}
render() {
return(
<button onClick={this.addMessage}>Add text</button>
)
}
}
class Messages extends React.Component {
constructor(props) {
super(props);
}
render() {
return(
<p>{this.props.message}</p>
)
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
messages: [{
text: 'first message'
}]
}
this.addMessage = this.addMessage.bind(this);
}
addMessage(message) {
let messages = this.state.messages.slice();
messages.push({text:message});
this.setState({messages: messages});
}
render() {
let messages = [];
this.state.messages.forEach((message) => {
messages.push(<Messages message={message.text}></Messages>);
});
return(
<div>
{messages}
<Button addMessage={this.addMessage} />
</div>
)
}
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.0/react-dom.min.js"></script>
<div id="root"></div>
Thanks in advance!
State Updates May Be Asynchronous
Quoted from the reactjs docs about state updates:
React may batch multiple setState() calls into a single update for
performance.
Because this.props and this.state may be updated asynchronously, you
should not rely on their values for calculating the next state.
Your second call to addMessage() uses this.state to set the new state. But because setState() is asynchronous the state my not have been updated yet which leads to your second call overriding your first one:
addMessage(message) {
// here you access this.state which hasn't been updated yet in your second call
let messages = this.state.messages.slice();
messages.push({text: message});
this.setState({messages: messages});
}
To fix it, use a second form of setState() that accepts a function
rather than an object. That function will receive the previous state
as the first argument, and the props at the time the update is applied
as the second argument:
Use an arrow function that gets passed the previous state including changes of preceding calls to setState()to calculate the new state. That will fix your issue.
Using es6 spread syntax:
this.setState((prevState, props) => ({
messages: [...prevState.messages, {text: message}]
}));
Using slice():
this.setState((prevState, props) => {
let messages = prevState.messages.slice();
messages.push({text:message});
return {messages};
});