Please see the example here http://jsfiddle.net/8xzxkteu/1/
I'm trying to only render part of the data which is changed. In this example, state of component Main, data, is indexed by id and I am using react immutability helper to set only the changed one. But, if you click on the output, it renders all the children, as indicated by the counter. I though using immutability helper react can detect only part of the data changed hence only render it. I probably could use shouldComponentUpdate and compare object values for each child, but is there a better way doing this with immutability helper.
class Child extends React.Component {
constructor(props) {
super(props);
this.onClick = this.onClick.bind(this)
this.state = {
count: 0
};
}
componentWillReceiveProps(nextProps) {
var count = this.state.count + 1;
this.setState({ count: count });
}
onClick() {
this.props.onClick(this.props.name);
}
render() {
return <p onClick={this.onClick}>{this.props.name}: {this.props.value} {this.state.count}</p>;
}
}
class Main extends React.Component{
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this)
this.state = {
data: {
"a" : "a",
"b" : "b",
}
};
}
handleChange(id) {
this.setState({
data: React.addons.update(this.state.data, { [id]: { $set: 'x' } })
});
}
render() {
const keys = Object.keys(this.state.data);
const children = keys.map(k => {
return <Child name={k} value={this.state.data[k]} onClick={this.handleChange}/>
})
return <div>
{children}
</div>;
}
}
React.render(<Main />, document.getElementById('container'));
When you change state of component react call shouldComponentUpdate of this component and if it is return true react call render of this component.
After that react call componentWillReceiveProps, then shouldComponentUpdate, then render (if shouldComponentUpdate return true) of all child component.
By default, if there no shouldComponentUpdate method, it is considered that it has returned true. It does not matter whether you use immutable data or not - react does not know about it.
If you have immutable data you want avoid rerender, you should use shouldComponentUpdate. You can use pure-render-decorator, for example – it's check component state and props.
But if you change your state in componentWillReceiveProps you still get rerender because componentWillReceiveProps is called before shouldComponentUpdate.
Related
My React structure is
- App
|--SelectStudy
|--ParticipantsTable
In SelectStudy there is a button whose click triggers a message to its sibling, ParticipantsTable, via the App parent. The first Child->Parent transfer works. But how do I implement the second Parent->Child transfer? See questions in comments.
App
class App extends Component {
myCallback(dataFromChild) {
// This callback receives changes from SelectStudy Child Component's button click
// THIS WORKS
alert('SelectStudy Component sent value to Parent (App): ' + dataFromChild.label + " -> " + dataFromChild.value);
// QUESTION: How to Update State of ParticipantsTable (SelectStudy's Sibling) next?
// ........................................................
}
render() {
return (
<div className="App">
<SelectStudy callbackFromParent={this.myCallback}></SelectStudy>
<ParticipantsTable></ParticipantsTable>
</div>
);
}
SelectStudy
class SelectStudy extends React.Component {
constructor(props) {
super(props);
this.state = {
error: null,
isLoaded: false,
items: [],
selectedStudy: null,
isButtonLoading: false
};
this.handleButtonClick = this.handleButtonClick.bind(this);
}
render() {
const { error, isLoaded, items, itemsForReactSelect, selectedStudy, isButtonLoading } = this.state;
return <Button onClick={this.handleButtonClick}>Search</Button>;
}
handleButtonClick = () => {
this.props.callbackFromParent(this.state.selectedStudy);
}
}
ParticipantsTable - this needs to receive a certain variable, e.g. study in its State
class ParticipantsTable extends React.Component {
constructor(props) {
//alert('Constructor');
super(props);
// Initial Definition of this component's state
this.state = {
study: null,
items: [],
error: null
};
}
// THIS METHOD IS AVAILABLE, BUT HOW TO CALL IT FROM App's myCallback(dataFromChild)?
setStudy = (selectedStudy) => {
this.setState({study: selectedStudy});
}
render() {
return ( <div>{this.state.study}</div> );
}
}
The state should live definitively at the App level, not in the child. State needs to live one level above the lowest common denominator that needs access to it. So if both SelectStudy and ParticipantsTable need access to the same bit of state data, then it must live in their closest common ancestor (or above).
This is a core concept of React, known as "lifting state up", so much so that it has its own page in the official React documentation.
In your case, it would look something like this. Notice how state lives in only one place, at the <App /> level, and is passed to children via props.
import React from 'react';
class App extends React.Component {
// State lives here at the closest common ancestor of children that need it
state = {
error: null,
isLoaded: false,
items: [],
selectedStudy: null,
isButtonLoading: false
};
myCallback = (dataFromChild) => {
this.setState(dataFromChild);
};
render() {
return (
<div className="App">
{/* State is passed into child components here, as props */}
<SelectStudy data={this.state} callbackFromParent={this.myCallback}></SelectStudy>
<ParticipantsTable study={this.state.selectedStudy} />
</div>
);
}
}
class SelectStudy extends React.Component {
handleButtonClick = () => {
// Here we execute a callback, provided by <App />, to update state one level up
this.props.callbackFromParent({ ...this.props.selectedStudy, isButtonLoading: true });
};
render() {
const { error, isLoaded, items, itemsForReactSelect, selectedStudy, isButtonLoading } = this.props.data;
return <Button onClick={this.handleButtonClick}>Search</Button>;
}
}
// This component doesn't need to track any internal state - it only renders what is given via props
class ParticipantsTable extends React.Component {
render() {
return <div>{this.props.study}</div>;
}
}
I think what you need to understand is the difference between state and props.
state is internal to a component while props are passed down from parents to children
Here is a in-depth answer
So you want to set a state in the parent that you can pass as props to children
1 set state in the parent
this.state = {
value: null
}
myCallback(dataFromChild) {
this.setState({value: dataFromChild.value})
}
2 pass it as a prop to the children
class ParticipantsTable extends React.Component {
constructor(props) {
super(props);
this.state = {
study: props.study,
items: [],
error: null
};
}
Also, although not related to your question, if you learning React I suggest moving away from class-based components in favour of hooks and functional components as they have become more widely used and popular recently.
This app is supposed to filter words by a specific input. I want to call a function with setState() when rendering a component and technically it's working but there is warning in the console.
Warning: Cannot update during an existing state transition (such as within render). Render methods should be a pure function of props and state.
I guess that this is because I'm calling the function in the render function which I shouldn't, but what should I do instead?
class UsersList extends React.Component {
constructor(props) {
super(props);
this.state = {
allUsers: ["Michał", "Ania", "Kasia", "Tomek", "Hubert", "Jan", "Martyna", "Rafał", "Bartłomiej"],
filteredUsers: [],
input: null
}
}
filter() {
if (this.state.input !== this.props.inputValue) {
const filtered = this.state.allUsers.filter(user => user.toLowerCase().includes(this.props.inputValue));
this.setState({
filteredUsers: filtered.map(user => <li key={user}>{user}</li>),
input: this.props.inputValue
})
}
return this.state.filteredUsers;
}
render() {
this.filter()
return (
<ul>
{this.state.filteredUsers}
</ul>
)
}
}
class App extends React.Component {
constructor() {
super();
this.state = {input: ""};
this.handleInput = this.handleInput.bind(this);
}
handleInput(e) {
this.setState({input: e.target.value})
}
render() {
return (
<div>
<input onChange={this.handleInput} type="search"/>
<UsersList inputValue={this.state.input} />
</div>
);
}
}
The issue here is caused by changes being made to your component's state during rendering.
You should avoid setting component state directly during a components render() function (this is happening when you call filter() during your component's render() function).
Instead, consider updating the state of your component only as needed (ie when the inputValue prop changes). The recommended way to update state when prop values change is via the getDerivedStateFromProps() component life cycle hook.
Here's an example of how you could make use of this hook for your component:
class UsersList extends React.Component {
constructor(props) {
super(props);
this.state = {
allUsers: ["Michał", "Ania", "Kasia", "Tomek",
"Hubert", "Jan", "Martyna", "Rafał",
"Bartłomiej"],
filteredUsers: [],
input: null
}
}
/* Add this life cycle hook, it replaces filter(). Props are updated/incoming
props, state is current state of component instance */
static getDerivedStateFromProps(props, state) {
// The condition for prop changes that trigger an update
if(state.input !== props.inputValue) {
const filtered = state.allUsers.filter(user => user.toLowerCase().includes(props.inputValue));
/* Return the new state object seeing props triggered an update */
return {
allUsers: state.allUsers
filteredUsers: filtered.map(user => <li key={user}>{user}</li>),
input: props.inputValue
}
}
/* No update needed */
return null;
}
render() {
return (<ul>{this.state.filteredUsers}</ul>)
}
}
Hope this helps
The error is coming up as it could create an endless loop inside the component. As render method is executed whenever the state is updated and your function this.filter is doing a state update. Now as the state updates, your render method triggers the function again.
Best way to do that would be in lifecycle methods or maintain the uses in the App and make UserList a dumb component by always passing the list of filtered users for it to display.
I have a state like this :
{
textfield: '',
data: [] //huge, used to render elements within the render()
}
When I want to update the textfield value (simple text input), I use this.setState({ textfield: newValue });. The problem is that there is some lag when I write a character in the field because it is re-rendering everything.
Is using shouldComponentUpdate() and deeply check my data object the only way to avoid re-rendering everything? Or is there a better/more efficient way?
Thanks
Am guessing its rerendering the entire component due to the state change on every key.
you could isolate your input element in a separate stateful component, hence only triggering a re-render on itself and not on your entire app.
So something like:
class App extends Component {
render() {
return (
<div>
...
<MyInput />
...
</div>
);
}
}
class MyInput extends Component {
constructor() {
super();
this.state = {textfield: ""};
}
update = (e) => {
this.setState({textfield: e.target.value});
}
render() {
return (
<input onChange={this.update} value={this.state.textfield} />
);
}
}
In this example
https://codepen.io/ismail-codar/pen/QrXJgE?editors=1011
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
static getDerivedStateFromProps(nextProps, prevState) {
console.log("nextProps", nextProps, "\nprevState", prevState)
if(nextProps.count !== prevState.count)
return {count: nextProps.count};
else
return null;
}
handleIncrease(e) {
this.setState({count: this.state.count + 1})
}
handleDecrease(e) {
this.setState({count: this.state.count - 1})
}
render() {
return <div>
<button onClick={this.handleIncrease.bind(this)}>+</button>
{this.state.count}
<button onClick={this.handleDecrease.bind(this)}>-</button>
</div>;
}
}
class Main extends React.Component {
constructor(props) {
super(props);
this.state = { initialCount: 1 };
}
handleChange(e) {
this.setState({initialCount: e.target.value})
}
render() {
return <div>
<Counter count={this.state.initialCount} />
<hr/>
Change initial:<input type="number" onChange={this.handleChange.bind(this)} value={this.state.initialCount} />
</div>
}
}
ReactDOM.render(
<Main/>,
document.getElementById("root")
);
Expected:
Clicking + / - buttons and textbox change must be update count
Currently:
Main component stores initialCount in own state and passes initial count to child Counter Component.
If handleChange triggered from textbox and initialCount is updated also child Counter component is updated correctly because getDerivedStateFromProps static method provides this.
But changing count value in Counter component with updating local state via handleIncrease and handleDecrease methods it prolematic.
Problem is getDerivedStateFromProps re-trigger this time and resets count value. But I did not expect this because Counter component local state updating parent Main component is not updating. UNSAFE_componentWillReceiveProps is working this way.
Summary my getDerivedStateFromProps usage is incorrect or there is another solution for my scenario.
This version https://codepen.io/ismail-codar/pen/gzVZqm?editors=1011 is good with componentWillReceiveProps
Trying to "sync" state to props like you do is extremely error-prone and leads to buggy applications.
In fact even your example with componentWillReceiveProps has a bug in it.
If you re-render the parent component more often, you will lose user input.
Here is a demo of the bug.
Increment counter, then click “demonstrate bug” and it will blow away the counter. But that button’s setState should have been completely unrelated.
This shows why trying to sync state to props is a bad idea and should be avoided.
Instead, try one of the following approaches:
You can make your component fully "controlled" by parent props and remove the local state.
Or, you can make your component fully “uncontrolled”, and reset the child state from the parent when necessary by giving the child a different key.
Both of these approaches are described in this article on the React blog about avoiding deriving state. The blog post includes detailed examples with demos so I strongly recommend to check it out.
I'm not sure if I understood correctly but if you want to use the prop as a "seed" for the initial value to do it in the constructor and you don't even need getDerivedStateFromProps. You actually don't need to duplicate state:
class Counter extends React.Component {
render() {
return <div>
<button onClick={this.props.handleIncrease}>+</button>
{this.props.count}
<button onClick={this.props.handleDecrease}>-</button>
</div>;
}
}
class Main extends React.Component {
constructor(props) {
super(props);
this.state = { count: 1 };
}
handleIncrease() {
this.setState(prevState => ({count: prevState.count + 1}))
}
handleDecrease() {
this.setState(prevState => ({count: prevState.count - 1}))
}
render() {
return (
<div>
<Counter count={this.state.count} />
<hr/>
Change initial:
<input
type="number"
handleIncrease={this.handleIncrease.bind(this)}
handleDecrease={this.handleDecrease.bind(this)}
count={this.state.count}
/>
</div>
)
}
}
I have a parent component Rides which sends data to child component via props.
Here is the Child component:
class LazyLoader extends React.Component {
constructor(props){
super(props);
this.state = {
loaderState: this.props.loaderState
}
}
componentWillReceiveProps(nextProps){
console.log(`inside componentWillReceiveProps ${nextProps.loaderState}`);
if(this.props != nextProps) {
this.setState({
loaderState: nextProps.loaderState
}, () => {console.log(`Finished setState for ${nextProps.loaderState}`)});
}
}
render(){
console.log(`loaderState: ${this.state.loaderState}`);
return (
this.state.loaderState ? (
<View style={[styles.container]}>
<ActivityIndicator style={styles.spinner} animating={true} size={60} color='#fff'/>
</View>) : (<View/>)
)
}
}
And part of my parent component (here Rides) which send the updated data from its state to child's(here LazyLoader) prop:
export default class Rides extends React.Component {
constructor(props){
super(props);
this.handleRidePress = this.handleRidePress.bind(this);
this.navigateToRideDetails = this.navigateToRideDetails.bind(this);
this.state = {
flash: true
};
}
handleRidePress(rideId){
this.setState({
flash: true
}, () => {
console.log(`Begining handleRidePress for rideId: ${rideId}`);
if(doSomeTimeConsumingOperation){
// Time consuming operation goes here
}
this.navigateToRideDetails({rideId: rideId, pingData:pingData.slice(), rideData: rideDetails});
});
}
navigateToRideDetails(rideObj){
this.setState({
flash: false
}, () => {console.log(`navigateToRideDetails setState cb`); this.props.navigation.navigate('RideLog', rideObj);});
}
render(){
console.log(`flash: ${this.state.flash}`);
return(
<Gradient.FullGradient>
<Reusables.LazyLoader loaderState={this.state.flash}/>
<PRides rides={this.state.rideTiles} onDeletePress={this.deleteRide} navigation={this.props.navigation} onRideDetailPress={this.handleRidePress}/>
</Gradient.FullGradient>
)
}
}
When handleRidePress function is called it updates the flash state using setState() but child component doesn't get rerender. I tried using shouldComponentUpdate() in both component and returned true by default, but It doesn't work.
I think error is in the way you are comparing two objects here:
if(this.props != nextProps) {
....
}
This is not the correct way to check whether two objects are same or not, check the values:
if(this.props.loaderState != nextProps.loaderState) {
....
}
Check this answer How to determine equality for two JavaScript objects?
Check this snippet:
let a = {b: true};
let b = {b: true};
console.log(a == b); //false
I modified navigateToRideDetails to execute setState independently as react batches state updates that occur in event handlers and lifecycle methods. Thus, if we are updating state multiple times in a function handler, React will wait for event handling to finish before re-rendering. In my case, the flash value in state was getting reset after operation gets completed. Hence ActivityIndicator was not getting displayed.
Here is modified navigateToRideDetails function
navigateToRideDetails(rideObj){
setTimeout(() => {
this.setState({
flash: false
}, ()=>{
this.props.navigation.navigate('RideLog', rideObj);
});
}, 0);
}
This solves the issue :)