Deep dive in React Hooks
A talk by Christophe Porteneuve at Station F • October 24, 2019
A talk by Christophe Porteneuve at Station F • October 24, 2019
const christophe = {
family: { wife: 'Élodie', sons: ['Maxence', 'Elliott'] },
city: 'Paris, FR',
company: 'Delicious Insights',
trainings: ['Modern Web Apps', 'Node.js', '360° Git', '360° ES', 'Webpack'],
webDevSince: 1995,
claimsToFame: [
'Prototype.js',
'Ruby On Rails',
'Prototype and Script.aculo.us',
'Paris Web',
'NodeSchool Paris',
'dotJS',
],
}
We used to reach for classes when needing state, lifecycle, refs, and more.
Most of this is now doable with function components!
Functions are easier to reason about, to compose, to optimize, etc.
Automatically avoids most of the pitfalls inherent to out-of-render behavior, mostly instance-stored data
(e.g. race conditions with async updates and shared scope).
Buggy class-based example:
|
Bug-free function-based example:
|
“Function components capture the rendered values”
(and with hooks, same for state!)
“Keep variables in sync”
“Code as if any value can change at any time”
No. Use a gradual adoption strategy. Also, getSnapshotBeforeUpdate()
(however rarely useful) and componentDidCatch()
(error boundaries) have no equivalent yet.
Making Sense of React Hooks (Dan Abramov, Nov. 2018)
How Are Function Components Different From Classes (Dan Abramov, Mar. 2019)
Thinking in React Hooks (gorgeous article by Amelia Wattenberg, Oct. 2019)
Writing Resilient Components (Dan Abramov, Mar. 2019)
Again, classes / OOP are harder to grasp than plain functions
Hard to reuse stateful logic across components
(Render Props and HOCs are cumbersome, both on the code side and the debugging side)
Custom hooks let us extract single-concern, reusable behavior in a snap!
Behavior is splattered all over the class (cdM/cwU pairs, duplicate code in cDM/cdU…)
Huge components become hard to refactor and test
Classes don’t lend themselves well to upcoming optimizations (AOT compilation, memoization, component folding…)
This is closer to React’s essence (React as a “pure” function turning state into UI)
Check out the Motivation for Hooks
Beta in Fall 2018
Official in React 16.8 (Feb 6, 2019)
React Native 0.59 (Mar 12, 2019)
Preact X (Oct 1, 2019)
function FriendStatusWithCounter({ friend }) {
const [count, setCount] = useState(0)
useEffect(() => {
document.title = `You clicked ${count} times`
})
const [isOnline, setIsOnline] = useState(null)
useEffect(() => {
ChatAPI.subscribeToFriendStatus(friend.id, handleStatusChange)
return () => {
ChatAPI.unsubscribeFromFriendStatus(friend.id, handleStatusChange)
}
})
function handleStatusChange(status) {
setIsOnline(status.isOnline)
}
// …
}
function TodoListItem({ id }) {
const dispatch = useDispatch()
const todo = useSelector((state) => state.todos[id])
return (
<li className={todo.done ? 'done' : 'todo'}>
<input type="checkbox" onChange={() => dispatch(toggleTodo(id))}>
{todo.text}
</li>
)
}
function Dashboard() {
usePageViews()
return (
<div className="dashboard">
<HomeButton />
{/* … */}
</div>
)
}
function HomeButton() {
const history = useHistory()
return (
<button type="button" onClick={() => history.push('/home'))}>Home</button>
)
}
function usePageViews() {
const location = useLocation()
useEffect(() => ga.send(["pageview", location.pathname]), [location])
}
function Awesomeness() {
const [text, setText] = useState('')
const [state, copyToClipboard] = useCopyToClipboard()
const { online, type } = useNetwork()
const { latitude, longitude } = useGeolocation()
return (
<div>
<p>At {latitude}°,{longitude}°, {online ? `online over ${type}` : 'offline'}</p>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button type="button" onClick={() => copyToClipboard(text)}>Copy text</button>
{state.error
? <p>Unable to copy value: {state.error.message}</p>
: state.value && <p>Copied {state.value}</p>}
</div>
)
}
useState
Local state: one major reason people used classes
setState
was often poorly understood through (delta, async, batched, obj. vs cb)
Can still do object-as-state, but will replace the whole thing: favor discrete state entries.
Arg is initial state; returned iterable is current value (as of rendering) +
function Workshop() {
const [attendees, setAttendees] = useState(0)
return (
<div>
<h1>There are {attendees} attendees</h1>
<button onClick={() => setAttendees(attendees + 1)}>
Welcome!
</button>
</div>
)
}
useState
Arg can be a callback for expensive initial-value computation (called only at mount).
function Table({ count }) {
const [rows, setRows] = useState(() => createRows(count))
// …
}
No separate default state initialization (in constructor)
No need to bind so we could call this.setState
, etc.
useEffect
For any side effect that doesn’t block rendering
(it’s critical for features such as Suspense and Concurrent Mode that rendering be interruptible)
Examples: data fetching, subscription management, DOM changes…
function Workshop() {
const [attendees, setAttendees] = useState(0)
const text = `There are ${attendees} attendees`
useEffect(() => { document.title = text })
return (
<div>
<h1>{text}</h1>
<button onClick={() => setAttendees(attendees + 1)}>Welcome!</button>
</div>
)
}
class
way. Ugh.)
class WorkshopBroadcast extends Component {
constructor(props) {
super(props)
this.state = { status: null }
this.handleStatusChange = this.handleStatusChange.bind(this)
}
componentDidMount() {
StreamingAPI.subscribeToWorkshop(this.props.workshop.id, this.handleStatusChange) // 1a
}
componentDidUpdate(prevProps) {
StreamingAPI.unsubscribeFromWorkshop(prevProps.workshop.id, this.handleStatusChange) // 2a
StreamingAPI.subscribeToWorkshop(this.props.workshop.id, this.handleStatusChange) // 1b
}
componentWillUnmount() {
StreamingAPI.unsubscribeFromWorkshop(this.props.workshop.id, this.handleStatusChange) // 2b
}
handleStatusChange(status) {
this.setState({ status })
}
render() {
if (this.state.status === null) { return 'Loading…' }
return this.state.status === 'future' ? 'Coming soon' : this.state.status
}
}
Callbacks return the cleanup function!
function WorkshopBroadcast({ workshop }) {
const [status, setStatus] = useState(null)
useEffect(() => {
StreamingAPI.subscribeToWorkshop(workshop.id, setStatus)
return () => { StreamingAPI.unsubscribeFromWorkshop(workshop.id, setStatus) }
})
if (status === null) { return 'Loading…' }
return status === 'future' ? 'Coming soon' : status
}
Much nicer than lifecycle (e.g. componentDidMount
, componentDidUpdate
, componentWillUnmount
).
Lets us keep concerns in one single place.
Used to be (unless you forgot to maintain it):
componentDidUpdate(prevProps) {
if (prevProps.workshop.id !== this.props.workshop.id) {
// …
}
}
With hooks, declare your effect dependencies:
useEffect(() => {
StreamingAPI.subscribeToWorkshop(workshop.id, setStatus)
return () => { StreamingAPI.unsubscribeFromWorkshop(workshop.id, setStatus) }
}, [workshop.id])
Careful about your effect dependencies.
Empty dependency list? Only at mount/unmount, not on updates.
function WorkshopBroadcast({ workshop }) {
const [status, setStatus] = useState(null)
useEffect(() => {
document.title = status === 'ongoing'
? `You’re watching ${workshop.title}`
: `${workshop.title}: ${status}`
}, [workshop, status])
useEffect(() => {
StreamingAPI.subscribeToWorkshop(workshop.id, setStatus)
return () => { StreamingAPI.unsubscribeFromWorkshop(workshop.id, setStatus) }
}, [workshop.id])
if (status === null) {
return 'Loading…'
}
return status === 'future' ? 'Coming soon' : status
}
The official hook documentation
A Complete Guide to useEffect (Dan Abramov, Mar. 2019, must-read!)
useContext
For context production/consumption. Yet another thing that had us reach for classes.
const stages = { junior: { name: 'Junior Stage', seats: 50 }, master: { name: 'Master Stage', seats: 352 } }
const StageContext = createContext(stages)
function Event({ workshop }) {
return (
<StageContext.Provider value={stages.junior}>
<Workshop workshop={workshop} />
</StageContext>
)
}
function Workshop({ workshop: { title } }) {
const stage = useContext(StageContext)
return (
<div className="workshop">{title} at {stage.name}!</div>
)
}
This means same exact list and order on every render
Ensures no mixup of internal state. Most importantly, lets us:
By far the most controversial design choice, yet absolutely necessary, as every alternative proposal has flaws.
Not regular JS functions outside of the FC or custom hook
This would make hooks harder to trace for developers, hindering maintenance and fostering bugs
Making sure we observe the rules is not always easy.
eslint-plugin-react-hooks to the rescue!
This is a major design goal of the hooks API: making it super-easy to extract reusable, stateful logic and blend it seamlessly in any other components. Used to require much harder patterns such as Render Props or HOCs, that were hard to get right and bloated the DevTools in a major way, among other drawbacks.
Let’s start from…
function WorkshopBroadcast({ workshop }) {
const [status, setStatus] = useState(null)
useEffect(() => {
StreamingAPI.subscribeToWorkshop(workshop.id, setStatus)
return () => { StreamingAPI.unsubscribeFromWorkshop(workshop.id, setStatus) }
})
if (status === null) {
return 'Loading…'
}
return status === 'future' ? 'Coming soon' : status
}
Now let’s extract the whole status-management part out of it:
function useWorkshopStatus({ id }) {
const [status, setStatus] = useState(null)
useEffect(() => {
StreamingAPI.subscribeToWorkshop(id, setStatus)
return () => { StreamingAPI.unsubscribeFromWorkshop(id, setStatus) }
})
return status
}
Nothing new: we just copy-pasted from above and simplified it a bit.
Our WorkshopBroadcast
now is:
function WorkshopBroadcast({ workshop }) {
const status = useWorkshopStatus(workshop.id)
if (status === null) {
return 'Loading…'
}
return status === 'future' ? 'Coming soon' : status
}
Sweet!
You should very much call your custom hooks useXxx
so the linter plugin can check it against the Rules of Hooks, by the way.
This naturally flows from the Hooks API design; it extracts stateful logic whilst retaining the automatic isolation of states we had when directly calling useState
, etc. It’s just functions after all!
Just look at stuff like react-use!
React Devtools have supported hooks since January 2019. |
For your custom hooks, you can use
|
You can technically use React’s test utilities with their ReactTestUtils.act()
function to test your hook-using components. This is a bit bare-bones and creates a lot of boilerplate, though.
The recommended stack from React and many others is Jest as a test harness (you betcha!) and React Testing Library (RTL), for more of an acceptance testing. Enzyme remains popular but is very unit-focused, requires full DOM testing and wrapper components over our hooks, so react-hooks-testing-library should do a better job for unit testing.
|
|
Much less boilerplate, and more user-oriented, so more resilient to changes…
import { render, fireEvent } from '@testing-library/react'
import '@testing-library/jest-dom/extend-expect'
it('can render and update an attendee count', () => {
// Test first render and effect
const { getByText, getByRole } = render(<Workshop />)
expect(getByRole('heading')).toHaveTextContent('There are 0 attendees')
expect(document.title).toBe('There are 0 attendees')
// Test second render and effect
fireEvent.click(getByText('Welcome!'))
expect(getByRole('heading')).toHaveTextContent('There are 1 attendees')
expect(document.title).toBe('There are 1 attendees')
})
Sometimes a ton of discrete states are not what you want, or multiple different calls to the setters get kludgy. “State that changes together should live together.” We used to bite the bullet and go full Redux on this, but if it’s still component-local, you can opt for a middle ground with the built-in useReducer
.
|
|
Need to use the state far down the tree? Pass (pre-bound) dispatch
through a context! (recommended)
useRef
Besides state, class-based components sometimes store stuff in “instance variables,” that is, expando properties on this
. For instance, DOM nodes, timer IDs and whatnot.
You also need, albeit very rarely, to “read from the future,” which means getting the latest value, at time of execution, regardless of what the value was at the time of the latest rendering (which you do usually want to use).
The useRef
hook will do just that. You could for instance use it as a clutch to retain references to “previous” values, such as previous state values.
Say you want to keep a timer interval around; if you change its delay, you need to clear the previous one and create a new one, sure. But if you don't change anything to it, or change its callback, you don't want to have its periodicity reset. You want a declarative timer interval system. How would you go about it?
useRef
in action: declarative timers
function useInterval(callback, delay) {
const savedCallback = useRef()
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback
}, [callback])
// Set up the interval.
useEffect(() => {
if (delay !== null) {
const id = setInterval(() => savedCallback.current(), delay)
return () => clearInterval(id)
}
}, [delay])
}
useMemo
: granular memoizationAt the component function level, React lets us prevent needless re-renderings based on shallow props comparison using React.memo()
, which is akin to PureComponent
for classes. But often we’d like a more granular approach, to optimize away needlessly redoing expensive computations.
This is what useMemo()
helps us achieve.
function Chart({ dateRange }) {
const data = useMemo(() => getDataWithinRange(dateRange), [dateRange])
// …
}
Just like React.memo()
, this is a hint, not a guarantee.
Don’t rely on it or algorithm correctness, only for performance optimization.
useCallback
There are two reasons to reach for useCallback()
.
First reason: helping optimized children not re-render because you’re passing a different callback every time. You would pass the same callback as long as nothing in its closure changes:
function NiceParent({ user, removable }) {
const onPingUser = useCallback(() => pingUser(user.id), [user.id])
return (
<div className="userPane">
<ActionsPane user={user} onPingUser={onPingUser} />
{removable && <UserRemover user={user} />}
</div>
)
}
useCallback
Second reason: you could rely on it for callback refs, to be notified any time the underlying node does get rendered, perhaps for measuring it, making sure to reuse a single callback across renders:
function YayTall() {
const [height, setHeight] = useState(0)
const measuredRef = useCallback((node) => {
if (node != null) {
setHeight(node.getBoundingClientRect().height)
}
// Or ES2020: setHeight(node?.getBoundingClientRect().height ?? 0) 😎
}, [])
return (
<>
<h1 ref={measuredRef}>Hey there!</h1>
<h2>The above header is {Math.round(height)} pixels tall, ya know…</h2>
</>
)
}
function useClientRect() {
const [rect, setRect] = useState({ width: 0, height: 0 })
const ref = useCallback((node) => {
if (node != null) {
setRect(node.getBoundingClientRect())
}
}, [])
return [rect, ref]
}
function YayTall() {
const [rect, ref] = useClientRect()
return (
<>
<h1 ref={ref}>Hey there!</h1>
<h2>The above header is {Math.round(rect.height)} pixels tall, ya know…</h2>
</>
)
}
useImperativeHandle
lets you allow refs on your FCs, customizing the imperative methods they expose.
Useful when your FCs forward refs.
useLayoutEffect
is for synchronous effects ⚠️😱 — you should rarely ever need it. The one use-case is essentially “read the DOM then synchronously re-render,” before the browser even has a chance to paint (same phase as componentDidMount
/componentDidUpdate
). Like, super-early layout measurement, or DOM mutation that would alter the layout (so you avoid flickering).
90% Cleaner React With Hooks from React Conf 2018 (33', free)
Simplify React Apps with React Hooks on Egghead (38', free)
Reusable State and Effects with React Hooks on Egghead (57', free)
Official docs (excellent)
Especially take a look at the Hooks FAQ once you’re comfortable
Most posts on Dan Abramov's blog. I especially recommend (in addition to already linked ones):
React as a UI Runtime (must-read for any React dev!)
Why Isn’t X a Hook? (lays a number of arguments to rest)
Making setInterval declarative with React Hooks (great use of ref hooks with imperative APIs)
Writing Resilient Components (always a good idea)
We publish about one new video course every month, mostly around Git and JS.
Some are even free!
My company, Delicious Insights, does absolutely kick-ass training classes on stuff you love:
Modern Web Apps stack,
Node.js,
Webpack,
Git (yes, you will learn a ton),
CSS Architecture (French only for now) and
100% of the latest ECMAScript.
Guess what?
And we’re just 20 minutes away.
Christophe Porteneuve
Slides are at bit.ly/stationf-react-hooks