Component Deep Dive
Container vs. Presentational Components
Typically, container components or components that manage state like App.js
shouldn't have a heavy render
method, i.e., contain a lot of JSX. Container components should simply contain the logic and state and state management behind an application.
You then pass methods and values to child components, which perform the rendering. These child components make up the majority of your components and will usually be presentational components.
Purpose: You split your components into container and presentational to make your app more manageable.
Component Lifecycle
Lifecycle hooks are functions that get invoked when certain events happen related to the class-based component.
Here are the phases of a component's lifecycle:
Creation
Update
Cleanup
Creation phase
When a class-based component is created, the following methods run in order:
Component immediately invokes the
constructor
method. If you don't write your ownconstructor
method, React will automatically invoke a default version for you. Therefore, you only writeconstructor
when you want to customize its behaviour.
Note: If you create your own
constructor
, you need to passprops
intoconstructor(props)
and runsuper(props)
. This is default behaviour that you now need to manually recreate.
Immediately afterwards, it invokes
getDerivedStateFromProps(props, state)
. You use this method to syncstate
to anyprops
passed in from external sources.
Note: This method must return
state
.Note 2: You won't use this often.
Then
render
method is invoked, returning JSX.
Important:
render
will invokerender
for all child components too! That means the original `render invocation won't finish until the entire DOM tree has rendered.
Finally,
componentDidMount
is invoked.
This is where you implement side effects.
Best practice: Never implement side effects in any of these steps except componentDidMount
. This includes HTTP requests, local storage, sending analytics to Google, etc. Side effects can trigger re-render cycles, which are bad for performance.
Deprecated lifecycle hooks at the creation phase:
componentWillMount
(replaced bygetDerivedStateFroProps
)
Updating phase
When state
or props
changes, these methods are run in order:
getDerivedStateFromProps(props, state)
is invoked again.shouldComponentUpdate(nextProps, nextState)
is where you can cancel the update process. You add code here to make a decision about whether the component should continue to evaluate and re-render.
Great for performance. Helps prevent unnecessary updates.
Note: This method must return
true
orfalse
. By default, it always returnstrue
.
render
then gets called, constructing the virtual DOM.
Important: This will update all your child components'
props
, triggering an update cycle for them too.
getSnapshotBeforeUpdate(prevProps, prevState)
is where you do last minute things before the update is complete like DOM operations. For example, maybe you want to grab the user's current scrolling position.
Note: This method must return what's called a snapshot, which gets passed as an argument to
componentDidUpdate
.
Finally, you end with
componentDidUpdate(prevProps, prevState, snapshot)
. You add side effects here.
Deprecated lifecycle hooks at the updating phase:
componentWillReceiveProps(props)
componentWillUpdate
Cleanup phase
Sometimes you have things like event listeners that you have to cleanup when a component is removed from the DOM. To do this, use componentWillUnmount
to add any custom code you want to run to perform the cleanup.
Lifecycle in Functional Components
useEffect
useEffect
is a React hook that you invoke inside a functional component that takes a callback function. The callback function gets invoked every time the component is rendered and re-rendered.
This mimics componentDidMount
and componentDidUpdate
pretty closely!
Note: You can invoke as many useEffect
methods as you want in your functional component.
Conditionally using useEffect
To mimic componentDidUpdate
alone, useEffect
takes a second argument after a callback that takes an array of variables that it watches. IF those variables change during a render cycle, then it will invoke the callback. Otherwise, it won't.
To mimic componentDidMount
alone, we add an empty array as the second argument of useEffect
.
Performing cleanup with useEffect
Any cleanup work is done by returning a function containing your cleanup code inside useEffect
.
Note: The returned cleanup method always runs before a re-render. That means it never runs on the first render cycle. It only runs before the next render cycle.
Dependencies in useEffect
There are 3 possible behaviours that useEffect can have depending on what you pass in the second argument:
No dependencies
Both the
useEffect
callback AND the returned cleanup method will run on every re-render.
[]
or empty arrayuseEffect
callback run on first render only. Cleanup method runs before second re-render, which is actually an un-render.
[foo, bar]
or dependenciesuseEffect
callback and cleanup method run if the dependencies changed.
Virtual DOM Optimization
Optimization with shouldComponentUpdate
Every time state
or props
changes, that triggers a full re-render of the virtual DOM. This has performance costs we can recuperate using shouldComponentUpdate
.
A great use case is container components re-rendering child components. By default, this always happens because child components get rendered recursively. However, there are many times when child components don't need to be re-rendered because nothing important changed for them.
In the example below, toggle
gets changed on button click, but the Child
is only affected when arr
changes. This leads to redundant re-rendering of Child
.
We only care if arr
changes. The solution is to use shouldComponentUpdate
to compare the reference between the old arr
and new arr
.
Note: The reason reference comparison works is that we created a copy of arr
using the spread operator, i.e., [...this.state.arr]
.
Pro tip: You can use the prototype PureComponent
instead of Component
too. PureComponent
has shouldComponentUpdate
built in, running a shallow comparison of all props
and state
.
Optimization with React.memo
React.memo
performs memoization on your functional components, storing a cache of the component. The component will then only re-render if its inputs change, i.e. props
. This minimizes unnecessary re-renders.
Recall that by default, when a parent component re-renders, it will re-render all its children too. So if, say, this.state.title
changes, child
will still re-render.
React.memo
solves this. Now, child
will only re-render when props.description
changes.
When should you optimize?
shouldComponentUpdate
and React.memo
still run code. That means they have computing costs. In cases where the child component always needs to update when the parent updates, those computing costs aren't worth it. You're better off defaulting to always re-rendering.
Best practice: Only apply shouldComponentUpdate
and React.memo
to components that unnecessarily re-render.
How React Updates the DOM
The basis of React is that it only updates the real DOM when it detects changes. Updating the real DOM is computationally expensive, so minimizing such changes is valuable. Here's how DOM changes happen in React:
When React invokes the
render
method, it creates a virtual DOM.On a re-render, React compares the current virtual DOM with the new virtual DOM.
If any differences are detected, those differences lead to real DOM updates only for the elements affected.
Note: shouldComponentUpdate
and React.memo
stop short this entire process because even this process has computational costs (though much less than updating the real DOM).
Adjacent JSX Elements
There are at least 2 ways to return adjacent JSX elements:
return
an array of JSX elements with keys for each element.
The problem with the array solution is that it's tedious to have to add a key.
Create an
Auxiliary
component that wraps around your adjacent JSX elements.
Explanation: React doesn't allow adjacent JSX elements because return
must return a single expression, and adjacent React.createElement
calls are multiple expressions.
Note: As of React 16.2, React ships with a React.Fragment
component that does the exact same thing.
Higher-order Components
Higher-order components wrap other components, adding something to those components: logic, error handling, styling, or JSX.
Examples include:
Auxiliary
WithClass
: wraps component with adiv
andprops.className
Note: By convention, higher-order components are named starting with With
like WithClass
.
Higher-order component structures
The first kind of higher-order component is a JSX wrapping component. You already saw it with Auxiliary
. Here's another example.
The second kind of higher-order component is a function that returns a functional component.
Pro tip: Although you can use either structure, it makes sense to use the JSX wrapping technique for JSX-related additions. Then use the functional component return technique for more logic-related additions. This helps with separation of concerns and keeping things compartmentalized.
More on props and state
Passing unknown props
Sometimes you want to pass an entire object of props
to a component, but you don't want to manually add them as JSX attributes one after another.
You might think this will work, but it won't:
That simply gives the component access to a props.props
object. Instead, you need to use the spread operator:
This spreads every key/value pair as if it was written like this:
Setting state correctly
When you invoke setState
, React doesn't immediately update state. Instead, React schedules a state update to be done when resources are available. In other words, you can't guarantee that your state is updating synchronously.
As a result, you want to avoid updating state with values that depend upon old state. Like this:
Instead, you want to pass a callback function that React guarantees will be the old state you expect.
Important: This is considered best practice when you're updating state with values dependent upon old state.
Using PropTypes
When you're working with other developers, you may want your props
to be explicit about what data types it accepts and what keys it accepts. To do this, just npm install prop-types
.
To use prop-types
, you just tack on an object map to your component as a property:
Now in development mode, your application will throw errors and warnings when the developer passes in the wrong data types or wrong attribute names.
Pro tip: For functions, you can even specify what arguments the function accepts and what it returns.
Using refs
Usually, props
is the only way to modify a child. However, sometimes we want to imperatively modify a child outside of the normal dataflow. For this, we use refs
to do things like:
Focus, text selection, media playback
Triggering animations
Note: Imperative programming in React means directly defining the flow of actions to do things like manipulate the DOM--like jQuery. Declarative programming simply sets up a roadmap of how actions should be performed.
There are 2 ways of making use of refs
.
Callback
refs
. In your component'sref
, pass a callback function that takes the reference to the element as an argument.
The React 16.3+ way is to use
React.createRef
.
Refs in functional components
Both use cases for refs
above work on class components. To do something similar with functional components, we need the useRef
hook.
Context and Props Chaining
When your app starts getting bigger and bigger, you'll find you're passing props
down multiple levels. This is messy and hard to manage. For example, passing props
from component A to component D is stupid when component B and C don't really care for or use the same props
.
Solution created by React: context. Context essentially creates a globally available JavaScript object for passing state--where you decide where it's available. (Context can be a string, array, or any other value too, not just an object.)
Implementing context
Create a
context.js
folder containing yourcontext
object.
In your container component carrying your state and thus providing context, wrap
<Context.Provider>
around the JSX code you want to gain access to context. Make sure to provide an object containing the state you want to pass into context in thevalue
attribute.
Finally, in the component using context, wrap
<Context.Consumer>
around the JSX code you want to use context. Make sure to provide a callback function inside the component wrapper. Context gets passed as an argument into the callback.
The below alternative approach is even better because it gives access to context in lifecycle methods, not just JSX!
useContext hook
For functional components, the useContext
gives us an easy way to add context.
Last updated