Why you should use useLayoutEffect when tinkering with focus

TL;DR

In this article, we will try to understand what useLayoutEffect is. I will attempt to demonstrate that you probably should:

  • Use regular effects with useEffect whenever possible to streamline the user experience.
  • Use regular effects to run asynchronous tasks as they will not be awaited anyway.
  • Use layout effects with useLayoutEffect whenever you need to run effects before the visual is painted to modify it.
  • Use layout effects when you need to handle the focus programatically, to avoid screen flickering.

So let’s get into it!

Recently I have been very busy with accessibility features for my projects. It really is a fascinating subject and there definitely is a lot to learn. But the first thing you really need to learn is handling the focus with the keyboard for your components because, not only some people can’t use a mouse when browsing, but some people might also just prefer using the keyboard to navigate.

Some context

What is useEffect?

useEffect allows you to run asynchronous side effects in your component. With its second argument, a dependency array, it is very useful for fetching data and using callbacks on state change.

You can learn more about useEffect in the official documentation.

What is useLayoutEffect and how does it differ from useEffect?

Unlike useEffect, useLayoutEffect runs synchronously after any DOM mutation — when React modifies the DOM during a render — but before the browser paints the page.

Figure 1. useEffect render timeline
Figure 2. useEffect render timeline on displayed page

Even though it is enticing to only use layout effects, it is strongly advised you start with useEffect and switch to useLayoutEffect only if necessary because any code that runs in the latter will cause the paint to be delayed. One of the use cases for layout effects is changing the content or layout of the page since regular effects could cause flickering:

Figure 3. Flickering page timeline with useEffect

By using useLayoutEffect instead of useEffect, the page will take longer to render but it will directly render with the meaningful content, removing the flicker effect of the multiple paints.

Figure 4. Flickering page timeline with useLayoutEffect

When you refresh the following sandbox, notice how the page renders the unmodified data for a fraction of a second before updating to the modified version with useEffect:

You might not be able to see the example below flicker if your computer manages to set the state before your browser paints the result.

https://codesandbox.io/s/useeffect-flicker-example-5uof2?from-embed

Figure 5. Flicker slowed down to 10% of base speed

In the example below using a layout effect, notice how the page is rendered directly with the modified data:

https://codesandbox.io/s/uselayouteffect-flicker-example-u98yt?from-embed

You can learn more about useLayoutEffect in the official documentation and in this article by Kent C. Dodds.

On accessibility

A small disclaimer, for the remainder of this article, we will only talk about focus and keyboard control but the topic of accessibility is way vaster, ranging from aria properties to roving tabindex and so much more. If you want to learn more about general accessibility, the official documentation from the W3C is an invaluable source of information provided you have some time on your hands.

Why would I want to handle the focus?

Did you know that your operating system is probably entirely usable without a mouse? On Windows and MacOS, you can navigate any shortcut using only the Tab key. Then, inside an application, a combination of Alt on Windows, ^+F2 on MacOS and arrow keys allows you to reach any menu. This is not just to be fancy, this is actually necessary for some people who can’t use a mouse. If any link or button on your page is not reachable using only the keyboard, it means that a portion of the users — and it is not just the people with a permanent disability, consider for instance someone with a temporarily broken arm or mouse — won’t be able to access it.

But I can make every link reachable just with tabindex, right?

Even though setting the tabindex attribute to 0 is sufficient to make any part of the application accessible, since it will add the element to the tab order, allowing it to be reached using the Tab key, it will still make the application very hard to use for someone with a disability. For your component to “feel native” and to facilitate the interaction with your components, you will need to implement keyboard controls:

Consider, for example, a screen reader user operating a tree [component].

Figure 6. Example of a tree component

Just as familiar visual styling helps users discover how to expand a tree branch with a mouse, ARIA attributes give the tree the sound and feel of a tree in a desktop application. So, screen reader users will commonly expect that pressing the right arrow key will expand a collapsed node. Because the screen reader knows the element is a tree, it also has the ability to instruct a novice user how to operate it. Similarly, voice recognition software can implement commands for expanding and collapsing branches because it recognizes the element as a tree and can execute appropriate keyboard commands.

W3C WAI-ARIA Authoring Practices

To expand on this tree example, a user might expect that pressing down arrow would move the focus to the next item, or pressing a character key would jump the focus to the next matching element, so now we need to implement a keyboard handler that takes these use cases into consideration. And since we need to instruct a screen reader that the focus jumped, we can’t implement our own version of focus() with some state and css classes. We need to use the built-in focus().

How do you give focus to a component?

There is no “React specific“ way of giving focus to a component since plain JavaScript allows us to do so very easily:

const input = document.getElementById('my-input')
input.focus()

So now the question is “How does it translate to React and its components?“.

We could technically still use document.getElementById():

function MyComponent() {
  const thisComponent = document.getElementById('my-component')
  thisComponent?.focus()
  return (
    <button type="button" id="my-component">Click Me!</button>
  )
}

But now we can only render this component once or we would have twice the same ID in our DOM, which is not allowed.

Introducing Refs!

A ref is essentially just a reference to some data that is shared between render instances but it can be used very effectively to reach the actual DOM element rendered by a React component:

import React, { useRef, useEffect } from 'react'

function App() {
  const buttonRef = useRef<HTMLButtonElement>(null)
  useEffect(() => {
    // buttonRef.current instanceof HTMLButtonElement --> true
  }, [buttonRef])
  return (
    <button type="button" ref={buttonRef}>Click Me!</button>
  )
}

Now, giving focus to the button is just plain JavaScript (or in this case TypeScript):

function App() {
  const buttonRef = useRef<HTMLButtonElement>(null)
  useEffect(() => {
    ref.current?.focus()
  }, [])
  return (
    <button type="button" ref={buttonRef}>Click Me!</button>
  )
}

So why do I need to use a layout effect when a regular effect seems to do the trick?

This far, I told you to avoid using layout effects when possible and now I’m telling you to use them when a regular effect seems to work?! I promise, I’m not just making things up: I would recommend using a layout effect in this instance because, when giving focus to an element, if not explicitly prevented, the browser will automatically scroll the element to view:

https://codesandbox.io/s/young-monad-sot2t?from-embed

This may cause the page to flicker if focus() is called on an element that is below the fold line since the viewport will jump to that element after the page is first painted:

https://codesandbox.io/s/jumping-focus-with-useeffect-r8xi5?from-embed

Figure 7. Flicker slowed down to 10% of base speed

This can be prevented with layout effects since the focus will already be on the last element when the browser paints the page:

https://codesandbox.io/s/jumping-focus-with-uselayouteffect-ubtiq?from-embed

Ok but when I fetch data, I change the content of the page, so why should I use regular effects?

The difference with handling focus is the asynchronicity of the network:

Figure 8. Timeline of an asynchronous fetch

At this point, your layout effect will create a Promise that will resolve long after the paint is done so you will delay your paint to create the Promise but it will not wait for your promise to resolve before painting. Using a layout effect in this case is detrimental since it will delay the paint but will not prevent the second render after the Promise resolves.

In contrast, using a layout effect to modify synchronously your DOM will prevent flickering:

Note that this use case is very limited, consider using your synchronous effect as the base value for your initial DOM mutation — e.g. as the default value for your state — to avoid mutating twice.

Figure 9. Timeline of a synchronous layout effect

And since focussing an element is not a DOM mutation, we don’t need to loop back on the DOM mutation step:

Figure 10. Timeline of a focus call in a layout effect

Conclusion Time!

We made it!

Let’s sum up:

  • Use regular effects with useEffect whenever possible.
  • Use layout effects with useLayoutEffect whenever you need to run effects before the visual is painted.
  • Do not use layout effects to run asynchronous tasks as they will not be awaited anyway.
  • When you need to handle the focus programatically, use layout effects to avoid flickering.

From the moment you start adding accessibility features to your application, you will need to manually handle the focus. Here is a snippet of code to help you:

function MyComponent() {
  const ref = useRef()
  useLayoutEffect(() => {
    if (shouldFocus) {
      ref.current?.focus()
    }
  }, [shouldFocus])
  return (
    ...
  )
}

Now this might get copy-pasted often so I’ll do you one better: what if we added a level of abstraction?

function MyComponent() {
  const ref = useFocus(shouldFocus)
  return (
    ...
  )
}

function useFocus(shouldFocus) {
  const ref = useRef()
  useLayoutEffect(() => {
    if (shouldFocus) {
      ref.current?.focus()
    }
  }, [shouldFocus])
  return ref
}

Want to learn more?

useEffect, useLayoutEffect and useRef

Accessibility

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.


Ce formulaire est protégé par Google Recaptcha