Deep dive in React Hooks

A talk by Christophe Porteneuve at Station F • October 24, 2019

whoami


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',
  ],
}
          

What are hooks?

Function components are dope!

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.

Function components are less tricky

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:

                    class ProfilePage extends Component {
                      showMessage = () => {
                        alert(`Followed ${this.props.user}`)
                      }

                      handleClick = () => {
                        setTimeout(this.showMessage, 3000)
                      }

                      render() {
                        return <button onClick={this.handleClick}>Follow</button>
                      }
                    }
                  
Bug-free function-based example:

                    function ProfilePage(props) {
                      function showMessage() {
                        alert(`Followed ${props.user}`)
                      }

                      function handleClick() {
                        setTimeout(showMessage, 3000)
                      }

                      return (
                        <button onClick={handleClick}>Follow</button>
                      )
                    }
                  

Function components are less tricky

What hooks boil down to…

“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”




Does this deprecate class-based components?

No. Use a gradual adoption strategy. Also, getSnapshotBeforeUpdate() (however rarely useful) and componentDidCatch() (error boundaries) have no equivalent yet.

Great reads on the underlying philosophy

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)

What’s wrong with class-based components?

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!

What’s wrong with class-based components?

Behavior is splattered all over the class (cdM/cwU pairs, duplicate code in cDM/cdU…)


What’s wrong with class-based components?

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

When?

Beta in Fall 2018

Official in React 16.8 (Feb 6, 2019)

React Native 0.59 (Mar 12, 2019)

Preact X (Oct 1, 2019)

Short examples

Short example : built-in hooks


            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)
              }
              // …
            }
          

Short example : React-Redux 7.1+


            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>
              )
            }
          

Short example : React Router 5.1+


            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])
            }
          

Short example : react-use library


            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>
              )
            }
          

Main built-in hooks

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) + setter .


              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))

              // …
            }
          

Also note:

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>
              )
            }
          

Effects with cleanup (the 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
              }
            }
          

Effects with cleanup (the hooks way)

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.

Skipping effects when unnecessary

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.

Best practice: one effect per concern


            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
            }
          

Grokking effects for realz

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>
                )
              }
          

The rules of hooks

(and why they’re needed) • Official docs here

Rule 1: Top-level only (no loops, no conditions)

This means same exact list and order on every render

Ensures no mixup of internal state. Most importantly, lets us:

  • Call the same base hook multiple times
  • Avoid any sort of name/scope clash
  • Extract custom hooks (major benefit of hooks!)

By far the most controversial design choice, yet absolutely necessary, as every alternative proposal has flaws.

Rule 2: Only call hooks from “React” functions

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

The ESLint plugin

Making sure we observe the rules is not always easy.

eslint-plugin-react-hooks to the rescue!

  • rules-of-hooks checks the rules themselves. Especially cool for top-level requirement.
  • exhaustive-deps makes sure your effects’ dependency lists appear to be complete.

The great power
of custom hooks

Achievement unlocked

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
            }
          

Extracting our hook

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.

There you go!

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!

Debugging hooks

React Devtools have supported hooks since January 2019.

A screenshot of hook introspection in React DevTools

For your custom hooks, you can use useDebugValue to inject custom labels in there.


                    function useWorkshopStatus({ id }) {
                      const [status, setStatus] = useState(null)

                      useEffect(() => {
                        StreamingAPI.subscribeToWorkshop(id, setStatus)
                        return () => { StreamingAPI.unsubscribeFromWorkshop(id, setStatus) }
                      })

                      useDebugValue(status)

                      return status
                    }
                  

Testing hooks

The docs are awesome

Choosing a testing stack

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.

Using regular Rest Test Utilities


                    let container = null
                    beforeEach(() => {
                      // Events wouldn’t propagate + reach React outside `document`
                      container = document.createElement('div')
                      document.body.appendChild(container)
                    })

                    afterEach(() => {
                      // Avoid leaks!
                      unmountComponentAtNode(container)
                      container.remove()
                      container = null
                    })

                    // (Continued right)
                  

                    // (Continued)

                    it('can render and update an attendee count', () => {
                      // Test first render and effect
                      act(() => {
                        ReactDOM.render(<Workshop />, container)
                      })
                      const button = container.querySelector('button')
                      const label = container.querySelector('h1')
                      expect(label.textContent).toBe('There are 0 attendees')
                      expect(document.title).toBe('There are 0 attendees')

                      // Test second render and effect
                      act(() => {
                        button.dispatchEvent(new MouseEvent('click', { bubbles: true }))
                      })
                      expect(label.textContent).toBe('There are 1 attendees')
                      expect(document.title).toBe('There are 1 attendees')
                    })
                  

Using React Testing Library

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')
            })
          

Other built-in hooks

Getting nifty with useReducer

(and how you might strip Redux)

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.


                    function reducer(state, { type }) {
                      switch (type) {
                        case 'INC':
                          return state + 1
                        case 'DEC':
                          return state - 1
                        case 'RESET':
                          return 0
                      }
                    }
                  

                    function Workshop() {
                      const [attendees, dispatch] = useReducer(reducer, 0)

                      return (
                        <div>
                          <h1>There are {attendees} attendees</h1>
                          <button onClick={() => dispatch({ type: 'INC' })}>Welcome!</button>
                          <button onClick={() => dispatch({ type: 'DEC' })}>Bye!</button>
                          <button onClick={() => dispatch({ type: 'RESET' })}>New session</button>
                        </div>
                      )
                    }
                  

Need to use the state far down the tree? Pass (pre-bound) dispatch through a context! (recommended)

The peculiar case of 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])
            }
          

Check out this article for full details.

useMemo: granular memoization

At 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>
                </>
              )
            }
          

DOM measurement: custom hook much?


            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>
                </>
              )
            }
          

Odds and ends

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).

Further reading

Videos

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)

Docs

Official docs (excellent)

Especially take a look at the Hooks FAQ once you’re comfortable

Further reading

Articles

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)

Do you like screencasts?

We publish about one new video course every month, mostly around Git and JS.


Check them out in English or French.

Some are even free!

Prefer in-room training?

We love’em too.


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?

🎁 Station F Founders teams get −15% 🎁

And we’re just 20 minutes away.

Thanks!

Enjoy React.


Christophe Porteneuve

@porteneuve

Slides are at bit.ly/stationf-react-hooks