Several Implementation Methods of Updating Components when props of Incoming Components in React Change

  react.js

When we use react, we often need to re-render a component when it receives props updates. The common method is tocomponentWillReceivePropsUpdate the new props to the component’s state (this state is called a Derived State), thus realizing re-rendering. React 16.3 also introduces a new hook functiongetDerivedStateFromPropsTo specifically meet this need. But whether it is usedcomponentWillReceivePropsOr ..getDerivedStateFromPropsNone of them are so elegant and prone to errors. So today, let’s discuss the problems that will arise from this kind of implementation and better implementation schemes.

When to use derived state

Let’s first look at a more common requirement, a list of users, which can add and edit users. When users click “New”
Button users can enter a new user name in the input box; When clicking the “Edit” button, the input box displays the edited user name, which can be modified by the user. The user list is updated when the user clicks the “OK” button.

class UserInput extends React.Component {

state = {
user: this.props.user
}

handleChange = (e) => {
this.setState({
user: {
...this.state.user,
name: e.target.value
}
});
}

render() {
const { onConfirm } = this.props;
const { user } = this.state;
return (
<div>
<input value={user.name || ''} onChange={this.handleChange} />
< button onclick = {() = > {onconfirm (user)}} > ok < /button >
</div>
);
}
}

class App extends React.Component {
state = {
users: [
{ id: 0, name: 'bruce' },
{ id: 1, name: 'frank' },
{ id: 2, name: 'tony' }
],
targetUser: {}
}

onConfirm = (user) => {
const { users } = this.state;
const target = users.find(u => u.id === user.id);

if (target) {
this.setState({
users: [
...users.slice(0, users.indexOf(target)),
user,
...users.slice(users.indexOf(target) + 1)
]
});
} else {
const id = Math.max(...(users.map(u => u.id))) + 1;
this.setState({
users: [
...users,
{
...user,
id
}
]
});
}
}

render() {
const { users, targetUser } = this.state;
return (
<div>
<UserInput user={targetUser} onConfirm={this.onConfirm} />
<ul>
{
users.map(u => (
<li key={u.id}>
{u.name}
< button onclick = {() = > {this.setstate ({targetuser: u})}} > edit < /button >
</li>
))
}
</ul>
< button onclick = {() = > {this.setstate ({targetuser: {})}} > new < /button >
</div>
)
}
}

ReactDOM.render(<App />, document.getElementById('root'));

After operation, the effect is as follows:
image

Now click on the “edit” and “new” buttons, the text in the input box will not switch, because when clicking on the “edit” and “update”, althoughUserInputProps of changed but did not trigger the update of state. Therefore, it is necessary to implement props changes to trigger the state update, which will be implemented inUserInputAdd code to:

componentWillReceiveProps(nextProps) {
this.setState({
user: nextProps.user
});
}

Or ..

static getDerivedStateFromProps(props, state) {
return {
user: props.user
};
}

This is how it is doneUserInputState is automatically updated each time a new props is received. But this implementation is problematic.

Problems Caused by Derived State

First, let’s define two concepts of components:Controlled data livesAndUncontrollered data lives. Controlled data refers to the data passed in through props in the component and affected by the parent component. Uncontrolled data refers to a state that is completely managed by the component itself, that is, an internal state. The derived state combines two data sources, and when the two data sources conflict, the problem arises.

Problem one

When modifying a user, click the “OK” button, and the text in the input box becomes the text before modification. For example, I changed’ Bruce’ to’ Bruce Lee’. After confirmation, the input box was changed to’ Bruce’, which we don’t want to see.

image

The reason for this problem is that click OK, App will re-render, and App will pass the previous user to as propsUserInput. Of course, we can click OK every time and thentargetUserReset to an empty object, but once the state is much, it is very difficult to manage.

question 2

Assuming that after the page is loaded, it will asynchronously request some data and then update the page. If the user has entered some text in the input box before requesting the page to be refreshed, the text in the input box will be cleared as the page is refreshed.

We can do it atAppAdd the following code to simulate an asynchronous request:

componentDidMount() {
 setTimeout(() => {
 this.setState({
 text: 'fake request'
 })
 }, 5000);
 }

The reason for this problem is that when the asynchronous request is completed,setStateAfterAppWill re-render while the component’scomponentWillReceivePropsIs executed each time the parent component render, and the passed inuserIs an empty object, soUserInputThe content of the has been emptied. AndgetDerivedStateFromPropsThe call is more frequent and will be called every time the component is render, so this problem will also occur.

In order to solve this problem, we cancomponentWillReceivePropsTo determine whether the newly incoming user is the same as the current user, if not, set the state:

componentWillReceiveProps(nextProps) {
if (nextProps.user.id !  == this.props.user.id) {
this.setState({
user: nextProps.user
});
}
}

Better solution

Uncertainties in data sources that derive state can lead to various problems, which should be avoided if each data has and is managed by only one component. There are two implementations of this idea, one is that the data is completely managed by the parent component, and the other is that the data is completely managed by the component itself. The following are discussed separately:

Fully controlled component

The component’s data comes entirely from the parent component, and the component itself will not need to manage the state. We have created a completely controlled version ofUserInput

class FullyControlledUserInput extends React.Component {
 render() {
 const { user, onConfirm, onChange } = this.props;
 return (
 <div>
 <input value={user.name || ''} onChange={onChange} />
 < button onclick = {() = > {onconfirm (user)}} > ok < /button >
 </div>
 )
 }
 }

Called in AppFullyControlledUserInputThe method is as follows:

...
 <FullyControlledUserInput
 user={targetUser}
 onChange={(e) => {
 this.setState({
 targetUser: {
 id: targetUser.id,
 name: e.target.value
 }
 });
 }}
 onConfirm={this.onConfirm}
 />
 ...

NowFullyControlledUserInputAll data in the comes from the parent component, thus solving the problem of data conflicts and tampering.

Fully uncontrolled component

The component’s data is entirely managed by itself, socomponentWillReceivePropsThe code in can be removed, but the incoming props is retained to set the initial value of state:

class FullyUncontrolledUserInput extends React.Component {
state = {
user: this.props.user
}

onChange = (e) => {
this.setState({
user: {
...this.state.user,
name: e.target.value
}
});
}

render() {
const { user } = this.state;
const { onConfirm } = this.props;
return (
<div>
<input value={user.name || ''} onChange={this.onChange} />
< button onclick = {() = > {onconfirm (user)}} > ok < /button >
</div>
)
}
}

When the incoming props changes, we can pass in a differentkeyTo recreate an instance of component to update the page. Called in AppFullyUncontrolledUserInputThe method is as follows:

<FullyUncontrolledUserInput
user={targetUser}
onConfirm={this.onConfirm}
key={targetUser.id}
/>

In most cases, this is a better solution. Some people may think that such performance will be affected, but the performance will not slow down much, and if the update logic of components is too complicated, it is better to create a new component quickly.

Call the child component’s method in the parent component to set the state

If there are no suitable attributes to act askey, then a random number or a self-increasing number can be passed as key, or we can define a method to set state in the component and passrefExposed to parent components, for example, we can useUserInputAdd:

setNewUserState = (newUser) => {
 this.setState({
 user: newUser
 });
 }

Call this method in App via ref:

...

<UserInput user={targetUser} onConfirm={this.onConfirm} ref='userInput' />
<ul>
{
users.map(u => (
<li key={u.id}>
{u.name}
<button onClick={() => {
this.setState({ targetUser: u });
this.refs.userInput.setNewUserState(u);
}}>
Edit
</button>
</li>
))
}
</ul>
<button onClick={() => {
this.setState({ targetUser: {} });
this.refs.userInput.setNewUserState({});
}}>
New
</button>

...

This method is not recommended unless it is really impossible. .

Please refer to the source code of this article:ways-to-update-component-on-props-change

Thank you for reading. In addition, I’m here to help my friends raise funds for love. I hope everyone can give some love. My friend’s mother is suffering from rectal cancer. She is currently receiving treatment in Beijing Armed Police General Hospital. Please leave a message and leave your contact information. Thank you in the future!

clipboard.png