React Gotchas, Anti-Patterns, and Best Practices
When to Bind Component Methods
When a React Component’s method uses this
, we need to be careful what this
refers to. Namely, we want this
to refer to the Component instance itself. This means we have to sometimes manually bind the function to the Component instance, but when?
Bind When Not Using Arrow Functions
If a Component was created using ES6 syntax and the method was defined without using an arrow function syntax, we need to manually bind the method’s this
to the Component instance in the constructor
method.
For example:
No Need to Bind When Using ES6 Component Classes with Arrow Functions
If a Component was created using ES6 syntax, but the method was defined using the arrow function syntax, you do not need to manually bind.
Avoid the Anonymous Function Hack if Possible
An alternative to binding is to pass an anonymous function to wrap around the method. This, however has performance issues around re-rendering. For example:
Because we are passing anonymous functions, React will always re-render since it receives a new anonymous function as a prop
which it is unable to compare to the previous anonymous function (since they are both anonymous). On the other hand, passing in a reference to the method like onClick={this.handleClick}
lets React know when nothing has changed so it does not unnecessarily re-render. However, the anonymous function is sometimes unavoidable as when we need to pass in an argument available only in the context: onClick={() => {this.handleClick(argHereOnly)}}
.
Adding Debuggers to Implicit Return Arrow Functions
Arrow functions are the de facto way of writing functions in React and Redux, and one variant of arrow functions is the implicit return arrow function, which is when the arrow is followed immediately by the return value wrapped in parenthesis, without a return
. For example,
const addTag = tag => ({
type: "ADD_TAG",
tag
});
But note we can’t add a debugger
in the return value as that would simply raise an error. So in order to add a debugger
, we must first convert it into a regular arrow function, one where a block immediately follows the arrow:
const addTag = tag => {
debugger
return {
type: "ADD_TAG",
tag
};
};
Understanding Component’s Props vs State
When should we use a component’s props
vs its state
?
Component Props
A component’s props
should be used for data that is passed into the component from the outside, typically from a parent component or the redux store. As such, a component cannot change its props
; change only comes from the outside when the component receives new props
. In this regard, props
can be thought of as a mechanism of communication between components. Typical props
include:
- a parent component’s state data
- a handler function from a parent component if the function needs that parent component’s context
- API or payload data from the redux store (via the
mapStateToProps
function) - dispatch functions from redux (via the
mapDispatchToProps
function)
Component State
A component’s state
should be used for data whose changes are managed by the component. Object-oriented programmers can think of the state
as the component’s member variables or instance variables. When a component mounts, its state
is initiated to a certain value but the component can change this state
over time through the setState
method. Typical examples of state
data include:
- data which controls the UI of a component (ie. disable/enable button on form, show/hide modal)
- data which originates from user triggered events (ie. entering input fields for controlled components — more on controlled components later)
- the passage of time (ie. a clock component)
Minimize Number of Stateful Components
A good rule-of-thumb is we should try to minimize the amount of stateful components and instead rely on passing props
into pure functional components whenever possible.
Don’t Rely on setState Being Synchronous
A gotcha with setState()
is that it is an asynchronous method, meaning it returns before actually setting the state
. As such, it’s advised against relying on the state
to have changed immediately after invoking setState
. For example:
Because of its asynchronous nature, setState
accepts a second argument that is a function that it invokes after the state
has been updated. So the above example would work if we rewrote handleFirstNameChange
as
handleFirstNameChange = (event) => {
this.setState({firstName: event.currentTarget.value}, () => {
console.log(this.state.firstName);
});
}
No Need to Always Initialize State in Constructor
If the initial state
is dependent on props
, then initialize the state in the constructor
as we need access to the props
.
class ProductRating extends Component {
constructor(props) {
super(props) this.state = {
starsSelected: props.starsSelected || 0
}
}
}
But if the initial state
is not dependent on props
, we can just initialize by setting it directly.
class ProductRating extends Component {
this.state = {
starsSelected: 0
}
}
We don’t always need to initialize state
in the constructor.
Component Lifecycle Gotchas and Deprecations
`Constructor` Gotchas
Much like the constructor method in other object oriented languages, constructor(props)
is used to instantiate the component, which is still not mounted at this point. Typical use cases for the constructor is setting the initial state
or binding methods to this
. Common gotchas in the constructor
method are:
- Make sure
super(props)
is the first line in theconstructor
method. - Unless you’re setting the initial
state
or binding methods tothis
, there’s no need to even have theconstructor
method. - Set the initial
state
bythis.state = { value: "foo" }
instead of usingsetState
as that would raise an error. This is becausesetState
will also re-render the component and its children, but this is not applicable in theconstructor
as the component has been rendered yet. - Do not make AJAX calls here since a component may be instantiated but never rendered, for example
loggedIn ? <Dashboard /> : <LogIn />
. If AJAX calls are made in theconstructor
forDashboard
but the user isn’t logged in, this would lead to unnecessary network calls and errors.
`componentWillMount` Has Been Deprecated
componentWillMount()
is invoked after the constructor(props)
but before render()
, meaning invoking setState()
in here will not cause an additional render
.
This method has been deprecated as of React 16.3, and after version 17, will be removed and replaced with UNSAFE_componentWillMount
. The reason is React 17 will introduce async rendering, which componentWillMount
may introduce bugs. For example, suppose a subscription was set in componentWillMount
and removed in componentWillUnmount
. Async rendering may cause the rendering to be interrupted before it completes, meaning componentWillUnmount
will not to be called and the subscription won’t be removed. This effectively makes componentWillMount
unsafe to use.
The general rule for migrating away from componentWillMount
is to move whatever was done in it into componentDidMount
.
`componentDidMount` Gotchas
componentDidMount
is invoked after the component has mounted and render()
is invoked, so this method can be thought of as initialization logic which requires the DOM to be present. Typical use-cases for componentDidMount
include making AJAX calls and setting up subscriptions.
A potential anti-pattern is calling setState
in this method as it would cause an additional re-render. If you find yourself setting the state
in this method, consider doing that in the constructor
instead. Setting the initial state
pretty much never depends on the DOM being present.
`componentWillReceiveProps` Has Been Deprecated
componentWillReceiveProps(nextProps)
is invoked right before a mounted component receives new props
. A typical use-case is to check if the nextProps
are different from the current props
and then set the state
or make an AJAX call.
This method has been deprecated as of React 16.3, and after version 17, will be removed and replaced with UNSAFE_componentWillReceiveProps
. The reason is that componentWillReceiveProps
will be unsafe due to async rendering. The suggestion is to use getDerivedStateFromProps(props, state)
for updating the state based on new props
. But note that getDerivedStateFromProps
is a static
method meaning it won’t have access to the component’s this
. Instead the state
is updated from its return value . This is intentional as it forces us to write pure functional methods without side-effects. For side-effects like making AJAX calls, use componentDidUpdate(prevProps, prevState, snapshot)
instead.
Controlled Forms, Uncontrolled Forms, and Redux-Controlled Forms
A Controlled Form is a form whose input values are determined by the Component’s state
. An Uncontrolled Form is a form whose input values are managed by the DOM instead. An Redux-controlled Form is a form whose input values are managed by the Redux store.
For example, consider a simple form with input values and default values written as a Controlled Form:
Now consider this form written as an Uncontrolled Form:
We could have also written this as a Redux-Controlled Form, where the input values are controlled by the redux store.
When To Use Controlled vs. Uncontrolled Form vs Redux-Controlled Forms
Uncontrolled Forms are simpler to write as it doesn’t need the synchronization code between state
and input values . However, Controlled Forms support more features such as dynamically validating input data or changing the UI based on input data such as validation errors that appear as the user types, or buttons being disabled depending on selection values.
Redux-Controlled Forms have the advantage of persisting form input data even after the user closes the form. This can enhance UI as when a user accidentally closes a partially-filled form. In this case, because the form input data is persisted in the redux store, the user does not have to re-enter the input fields upon re-opening the form.
The general rule is that simple forms can use Uncontrolled Forms. Forms that require on-the-fly UI changes should go with Controlled Forms. For forms that persist input values even after being closed, use Redux-Controlled Forms.
Optimizing Performance Through Pure Components
The rationale for `shouldComponentUpdate` and Pure Components
By default, a React Component always render
s when setState
is invoked or when its Parent render
s. But if the new state
and new props
are the same as before, re-render
ing is wasteful. This is why components have a method called shouldComponentUpdate
which defaults to return true
but which could be monkey patched to improve performance by reducing un-necessary renders.
shouldComponentUpdate
receives two arguments nextProps
and nextState
and returns a boolean determining dictating whether or not the Component should re-render. A very reasonable choice for shouldComponentUpdate
would be to shallow compare the current state with the next state and the current props and the next props, and only render if they are different. Why only a shallow compare instead of a deep compare? Because a deep compare can be even costlier than re-rendering, which defeats the purpose.
Performing a shallow comparison in shouldComponentUpdate
would look something like:
But React already provides a type of Component called Pure Components which implements the shouldComponentUpdate
method shown above. We use Pure Components by extending them, much like regular Components:
class UserComponent extends React.PureComponent
These Pure Components can improve app performance by reducing un-necessary re-renders but must be used carefully because it only performs shallow comparisons, which is why immutability of state
is important.
Immutability
Because Pure Components perform only a shallow comparison we need to work with immutable state
, meaning we should never change the existing state
, but always create a new copy of the state
with updated values.
This is because equality in Javascript depends on the data type. Primitive types like numbers, strings, booleans are considered equal if their values are equal. Reference types like objects and arrays are considered equal if their references are equal, even if those references have changed in value. As such, we SHOULD NOT MUTATE STATE BY DIRECTLY MANIPULATING THE STATE OBJECT LIKE THIS:
Because user
is an object and items
is an array, and equality of them is determined referentially, React would consider the user
and items
unchanged and would not re-render causing hard to find bugs. Instead, we must use the setState
and the spread operator.
setState
will ensure the Component re-renders while the spread operator returns a new object/array with the updated value(s), instead of modifying the old object/array. As such, when shouldComponentUpdate
does the shallow comparison, it will notice the object/array reference has changed and will re-render as we would expect.
Feel free to leave comments, questions, suggestions, corrections below. — S