Notes and annotations for Egghead's Simplify React Apps with React Hooks
Folder structure from this gist.
Table of Contents
- 02. Refactor a Class Component with React hooks to a Function
- 03. Handle Deep Object Comparison in React's
useEffect
hook with theuseRef
Hook - 04. Safely
setState
on a Mounted React Component through theuseEffect
Hook - 05. Extract Generic React Hook Code into Custom React Hooks
- 06. Track Values Over the Course of Renders with React
useRef
in a CustomusePrevious
Hook - 07. Refactor a React Class Component with useContext and useState Hooks
- 08. Refactor a render Prop Component to a Custom React Hook
- 09. Handle componentDidMount and componentWillUnmount in React Component Refactor to Hooks
- 10. Dynamically Import React Components with React.lazy and Suspense
- 11. Preload React Components with the useEffect Hook
Firstly, hooks are only available to function components. The first step is to begin moving class properties to function parameters.
Takeaways:
- hooks can only be used in function components
componentDidUpdate
containing effects is a good indicator of having to useuseEffect
with the properties being compared as the values that need to be provided inuseEffect
s arrayuseReducer
can be used to replace state in a class component. It accepts a reducer, and the initial state, and returns a state object and the dispatch function for state to be updated. The first argumentuseReducer
accepts is the current state.static contextType = My.Context
can be replaced withconst context = useContext(My.Context)
Before the previous refactor we were using lodash/isEqual
to do a deep
comparison of the variables
argument passed into the component. If that
property happens to be an object, and because it's passed down from the parent
component, React will determine that the object is not equal, and fire the
effect on every render.
We need to ensure that a deep comparison of the variable is done to prevent unnecessary requests.
To do this:
- remove the dependencies array from
useEffect
- compare the previous inputs with the new inputs from within
useEffect
- create a ref in which to store the previous inputs without forcing a render when values are updated in the ref
- create another
useEffect
which will be responsible for storing the previous inputs once the queryuseEffect
has run
Takeaways:
- one can do a manual comparison from within
useEffect
instead of relying onuseEffect
s dependecy array which will only perform shallow equality checks - the order of
useEffects
within a component matter - they are executed synchronously - values in a ref are stored on the
current
property useRef
allows one to create a ref, and refs can store anything outside of a component's state or props and be updated without forcing a re-render
Because useEffect
is executing an asynchronous request and setState
, the
dispatch
function returned from useReducer
, is called when the promise is
resolved or rejected, we need to ensure that if the component is unmounted that
setState
is not called, as useState
and useReducer
s returned updater
functions can't be called on unmounted components without throwing errors.
To fix this, we can use:
- a ref to store the mounted state of the component
- use
useEffect
to set the ref to indicate that the component is mounted - only call
setState
if the ref indicates the component is mounted - return a callback from within
useEffect
that will set the ref to indicate the component is no longer mounted - provide an empty deps array to
useEffect
to ensure that it is only called once on mount, and once on unmount
Takeaways:
- asynchronous operations within
useEffect
should have guards to ensure that calls to the component's functions are not executed if the component is no longer mounted - a ref is a good place to store this state, as we don't want rerenders to be triggered when it is updated
useEffect
s return callback is a sort of teardown function which can be used to undo whatuseEffect
does- an empty array as deps for
useEffect
will result in the effect only being executed on mount, and on unmount
One can create a custom hook outside of a component that can then be used inside the component. This is useful for hooks that are common, and less so for hooks that are within a single component only.
Takeaways:
- a custom hook can make use of another custom hook and extend it
useRef
can be used inside the custom hook in the same way as within the component, allowing one to abstract the logic for whether a component is mounted or not
One can extract useRef
to a custom hook. In this scenario we've extracted a
hook that could be useful for other components that need to evaluate props from
previous renders.
Takeaways:
- the order of hooks is important - the
usePrevious
hook in this example would be of no value if it were called before any code that needed to evaluate the previous render's props
A simple refactor of a class component to a component function:
- replace
static contextType
andthis.context
withuserContext
- replace component state with
useState
A render prop is useful, but creates a lot of nesting. By moving the props a render prop provides to a custom hook, one can remove the nesting, and import the custom hook instead.
Furthermore, a render prop can be created from the custom hook by simply creating a component function that accepts props from a parent, and returns the custom hook with those props appled:
const useCustomHook = () => {}
const MyRenderProp = props => useCustomHook(props);
Takeaways:
-
render props can be refactored to custom hooks (given the render prop doesn't have any of its own markup to render)
-
this can be done by:
1. removing `children` from the render prop's props 2. returning state directly 3. exporting a component function that wraps props passed in and returns the custom hook so that we still have a render props component 4. replacing the render prop in the component with the custom hook, getting the state provided by the old render prop from the new custom hook
Takeaways:
useEffect
should not be thought of in terms of being equivalent tocomponentDidMount
andcomponentWillUnmount
- a more accurate mental model is that it's a combination of the two andcomponentDidUpdate
useEffect
doesn't run synchronously after the first render, ascomponentDidMount
does; it runs asynchronously some time after the first renderuseState
accepts an initialiser function which is run only on first render, and from which the initial state is returned
Takeaways:
React.lazy
accepts a function that returns the promise from a dynamic import- lazy-loaded components need to be wrapped inside a
Suspense
component - React's
Suspense
component renders a fallback component while lazy-loaded components are in a pending state - An error boundary is required to prevent React from unmounting the app when unhandled exceptions are thrown
When users visit the home, we know that they're going to go to the user page
next. We're already dynamically loading the routes using React.lazy
, but these
chunks will only load when a user navigates to the specific routes.
Once the user route has loaded, only then will the request for user data begin.
We can improve on this by preloading the user page from inside the home page, since we know that users will be navigating to the user page from the home page.
To achieve this:
- use
useEffect
inside the home page - execute the function in
useEffect
only once, since we only want the chunk loaded once, by providing an empty dependency array - use Webpack's
import
to import the user page insideuseEffect
Takeaways:
- components that are being lazy loaded may do well to be preloaded if we know that a user will be interacting with them from some previous location
- this would tie in well with UIs that are navigated deterministically
useEffect
can be used along with Webpack'simport
from within a component in order to preload components from components we are confident will be preceding the preloaded component