SmartHR Tech Blog

SmartHR 開発者ブログ

React 19 で変わるアクセシビリティ周りの技術

こんにちは。アクセシビリティ本部のアクセシビリティエンジニアの五十嵐です。SmartHRでは主にアクセシビリティテスターが見つけた課題を技術的な観点から改善したり、根本的な問題を解決するための仕組みづくりを担当しています。

さて、Meta が開発する UI ライブラリとして長い間人気を博している React ですが、2024年4月に最新版であるバージョン 19 のRC版が公開されており、注目を集めています。

バージョン 19 では "use client""use server" でも知られる Server Components を含む様々な機能が含まれる予定ですが、この記事では、そんな React バージョン 19 をアクセシビリティの観点からキャッチアップし、特に便利になりそうな点や、注意が必要になりそうな点などを見ていきます。

forwardRef が不要になった

仮想 DOM の上に構成された UI ラブラリである React では、React コンポーネントのインスタンスとその DOM ノードの実体を紐づけるためにひと工夫が必要です。バージョン 18 以前では、React ノードを返す関数に forwardRef という関数を適用する必要がありました。

forwardRef では、適用した関数の第二引数に ref オブジェクトへの参照が渡され、それを HTML 要素を表すコンポーネントの ref prop に指定することで、ようやく DOM ノードの実体を得ることができました。*1

// 定義側
export const MyComponent = forwardRef((props, ref) => {
//                         ^^^^^^^^^^ これが必要だった
  return <div ref={ref}>...</div>;
});

// 利用側
const ref = useRef();
<MyComponent ref={ref} />;

しかし、バージョン 19 では JSX ランタイムの改善によってこの手続きが不要になります。「ref as prop」と呼ばれているこの機能は、ref というプロパティ名でも forwardRef でコンポーネントをラップすることなく DOM ノードへの参照を用いることができます。

// 定義側
export const MyComponent = ({ ref }) => {
  return <div ref={ref}>...</div>;
};

// 利用側
const ref = useRef();
<MyComponent ref={ref} />;

さて、この変更はアクセシビリティにとっては何を意味するのでしょうか?

Webを利用するユーザーの中には、マウスやトラックパッドといったポインティングデバイスを用いるユーザーの他に、キーボードやスクリーンリーダーといったデバイスで要素間を移動しながら閲覧するユーザーもいます。

そのような利用者にとっては「現在どの要素にフォーカスが当たっているか」が非常に大きな意味を持ちます。実際、アクセシビリティのテストや開発支援などを行う非営利組織「WebAIM」が2023 年 12 月に行ったアンケート「Screen Reader User Survey #10」では、スクリーンリーダーの利用者が最も問題だと感じる障害として「キーボード向けのアクセシビリティの欠如」が 5 番目に挙げられています。

特にReactで作られたWebサイトでは、動的なコンテンツの追加や削除によってフォーカスの順番が入れ替わってしまいやすくなっているため、細心の注意を払う必要があります。

例えば以下のようなドロップダウンメニューでは、メニューを開くボタンを押下した後にHTMLElement.focus などのDOM APIを用いて明示的にフォーカス位置をメニューの内部に移動させなければ、利用者がTabキーや矢印キーを何度も押し、自分でメニュー項目を探す必要があります。

W3Cの実例集ARIA Authoring Practicesによるドロップダウンメニューの例。ボタンが押下されたら、自動的に1つめのメニュー項目にフォーカスが移動する。

しかし、バージョン 18 以前の React では、フォーカスを管理する Element#focus の対象となるDOM要素への参照 ref を取得するために、forwardRef が必要でした。これは、Reactでのフォーカス管理の実装を妨げる一つの要因になっていたでしょう。

一方で、バージョン 19 では、上述の変更によってより簡単にフォーカス管理に対応することができるようになります。例えば、上記メニューのようなコンポーネントでは、次のように ref を管理できます。

// そのまま `ref` を受け取ることができる
const MenuItem = ({ ref, children }) => {
  return (
    <a href="..." ref={ref}>
      {children}
    </a>
  );
};

const Menu = () => {
  const firstItemRef = useRef();

  const handleClick = () => {
    firstItemRef.current.focus();
  };

  return (
    <>
      <button onClick={handleClick}>Actions</button>
      <ul>
        {/* 特別なことをせずにrefを使える */} 
        <li><MenuItem ref={firstItemRef}>Action 1</MenuItem></li>
        <li><MenuItem>Action 2</MenuItem></li>
        <li><MenuItem>Action 3</MenuItem></li>
      </ul>
    </>
  );
};

他にも、ARIA Authoring Practices で紹介されているような、モーダルダイアログやコンボボックスなどの動的な UI コンポーネントをReactで実装する際には、特にフォーカス管理に役立つはずです。

Server Components と Server Actions

Server Components と Server Actions は、バージョン 19 で stable となる目玉機能の1つです。ここでの詳説は避けますが、ディレクティブ "use server" を用いて定義した関数をコンポーネントのコールバックに渡すことで、サーバー上で関数を実行し、それをクライアント側でシームレスに利用できる機能と言えます。

「Next.js Conf 2023」での発表が記憶に新しいように、これによりデータ取得などのロジックを、API などを定義することなく簡潔に書けるようになることは広く知られていると思います。

Next.js Conf 2023 での Sam Selikoff 氏の発表How Next.js is delivering React’s vision for the future

しかし、 Server Components および Server Actions にはサーバーサイドの処理がコンポーネントに組み込めることの他にも大きな利点があります。それの1つは、初回ページ読み込みおよびFCP(First Contentful Paint)の速度が向上することと、クライアントサイドに含まれる JavaScript が削減されることです。*2

これには、Server Components や Server Actions としてマークされたコンポーネントや関数がStatic Rendering として処理され、クライアントに送信されるのはプレーンな HTML と、ストリーミングのための僅かな script 要素になることに起因しています。*3

さて、これはアクセシビリティにどのように関係するでしょうか?

まず、ダウンロードする JavaScript の量が削減されることは、ネットワークが低速なユーザーの体験を向上させます。さらに、JavaScript をパースし、実行する必要がなくなることから、性能が低いデバイスでのパフォーマンスも向上させることを意味しているでしょう。

また、安定したネットワークや、高性能なデバイスを利用しているユーザーでも、外出時にモバイルデータ通信が速度制限にかかったときや、バックグラウンドで別のタスクが実行されている時など、一時的にこれらのユーザーと同じ状況に置かれることは考えられます。これは、画像の代替テキストがスクリーンリーダー以外でも役に立つことと似ています。

Chromeにおいて、ネットワークなどが原因で`img`要素の画像の読み込みに失敗した際の表示の例。画像の代わりに代替テキストの値が表示される。

加えて、JavaScirpt それ自体が利用できないユーザーに対しても恩恵があるでしょう。ブラウザの設定で JavaScript を明示的に無効化している割合の統計情報は少なく、2013年の英政府の調査によると 1%未満だとされています。しかし、JavaScriptを明示的に無効化していなくても、ブラウザのリーダーモードを使っていたり、広告ブロッカーを利用しているなど、部分的にJavaScript が利用できないシーンは考えられます。

Safari のリーダービュー機能の例。CSS と JavaScript が無効化されていて、動的なコンテンツは利用できなくなる。

このように「最新のブラウザや高性能なデバイスが利用できる環境に対しては最適なユーザー体験を提供しつつ、制限がある環境でも同等の機能が利用できるよう後方互換性を維持する」という考えは、一般にProgressive Enhancement(漸進的な強化)という言葉でも知られています。*4

Actions とフック

上述の Server Actions に関連して、React バージョン 19 では transition でラップされた非同期関数が「Actions」という新しい概念として抽象化されるようになります。これにより、ボタンを押下したときや、フォームを送信したときに作られるイベントを、共通のインターフェイスから利用できるようになります。*5*6

Actions と共に利用可能になる新しいフックとして、 useActionStateuseFormStatus 2つがあります。引数として渡すデータや、呼び出せるコンポーネントの違いはありますが、返り値として利用できるデータは一部共通しています。

useActionState は Action を元にステートを更新するフックで、主にform 要素などと組み合せて使うことができます。主にリクエストの送信状態や、結果のデータをコンポーネント側から利用するときに用いられます。

一方、useFormStatusreact-dom パッケージから提供される最初のフックで、ツリーを遡って最も近くにある form の状態を、まるで Context API のように利用できる機能です。

ここでは例として、useFormStatus を用いて説明します。以下のように呼び出すことで、子コンポーネントである SubmitButtonから「送信中であるかどうか」や「結果のデータはどうなっているか」といった親フォームの状態を利用できます。*7

import { useFormStatus } from "react-dom";

export const MyForm = ({ action }) => {
  return (
    <form action={action}>
{/*  ^^^^ ここが Context Provider のようになる */}
      <label>
        <input type="text" name="name" />
      </label>

      <SubmitButton />
    </form>
  );
};

const SubmitButton = () => {
  const status = useFormStatus();
//      ^^^^^^ 親フォームの状態を利用できる
  return <button type="submit">Submit</button>;
};

さて、これらの新しいフックはアクセシビリティにどのように関係するでしょうか?

まず、useFormStatus の返り値である pending プロパティには、フォームが送信中であるかどうかを示す真理値が入っています。これを利用して、 button 要素に disabled 属性を付けることができます。

const SubmitButton = () => {
  const { pending } = useFormStatus();
  return (
    <button type="submit" disabled={pending}>
      Submit
    </button>
  );
};

従来は、loadingなどといったプロパティ名で各コンポーネントが独自の prop を設ける必要がありました。これはコンポーネント間の一貫性を損なうほか、prop を設定し忘れて二重送信ができるバグなどを引き起こす可能性がありえました。Actionsの登場によって、コンポーネント間のインターフェイスの差異を気にする必要がなくなります。

ライブリージョンとの組み合わせ

発展的な用途として、ライブリージョンとの組み合わせがあります。ライブリージョンとは、スクリーンリーダーなどの支援技術に対して動的なアナウンスを提供する機能で、通知や読み込みバーなどのページが更新されずに書き換わるような部分に WAI-ARIA 属性を設定することで利用できます。

例として、次の LoadingStatus のような読み込み状態を表示するようなコンポーネントでも useFormStatus は有効です。状態を示すコンポーネントは、送信されようとしているフォームの FormData インスタンスや、 pending 状態などを、最も近いフォームから取得することができます。

export const LoadingStatus = () => {
  const { data, pending } = useFormStatus();

  return (
    <p role="status">{pending ? `${data?.get("username")}に変更中...` : ""}</p>
  );
};

実際に動作している様子は次のようになります。

VoiceOver を使って上記のフォームを試す例。送信ボタン押下後、「(ユーザー名)に変更中」というアナウンスが流れる。

まとめ

この記事では、React の最新版であるバージョン 19 の新機能および変更点を、アクセシビリティの観点からキャッチアップしてきました。

まず、ref prop から DOM ノードへの参照を得るために必要だった forwardRef API が不要になったことを確認しました。これは、DOM ノードへの参照を用いてキーボード遷移や自動スクロールなどを行う必要があるドロップダウンメニューやモーダルダイアログで有用であることについて言及しました。

また、 Server Components および Server Actions の stable 化について紹介しました。これにより、ネットワーク速度が遅い時、デバイスの性能が限られている時などでもフォームなどのインタラクティブな要素を機能させることができることを学びました。

最後に、上述の Actions とuseFormStatus フックを組み合わせることで、下位のコンポーネントからフォームの状態を取得できることを確認しました。これにより、UI コンポーネント間で統一したインターフェイスが提供できることを学びました。

React バージョン 19 のリリース予定はまだ明らかではありませんが、これらの機能は RC にもなっていることから、バージョン19で利用できるようになるものと思われます。今後も、アクセシビリティを取り巻くフロントエンドの動向を注視していきたいです。

We Are Hiring!

SmartHR アクセシビリティ本部では、一緒にプロダクトのアクセシビリティを改善し、誰にでも使えるプロダクトを作る仲間を募集しています。

アクセシビリティエンジニアはこの記事で紹介したようなフロントエンドの知識や、React の知識も活かせるポジションとなっています。

少しでも興味を持っていただけたら、下記リンクからご応募いただけますと幸いです。

*1:"sometimes it’s useful to expose a DOM node to the parent—for example, to allow focusing it. To opt in, wrap your component definition into forwardRef()" https://react.dev/reference/react/forwardRef#exposing-a-dom-node-to-the-parent-component

*2:"There are a couple of benefits to doing the rendering work on the server, including..." https://nextjs.org/docs/app/building-your-application/rendering/server-components

*3:"This is beneficial for users with slower internet or less powerful devices, as the browser has less client-side JavaScript to download, parse, and execute." https://nextjs.org/docs/app/building-your-application/rendering/server-components

*4:「あらゆるユーザーに対して、基本的な体験ができるようにします。その上で、広い帯域幅や高機能なブラウザを使っているユーザーには、より豊かな体験を提供します。」 https://accessible-usable.net/2010/06/entry_100606.html

*5:"By convention, functions that use async transitions are called “Actions”. " https://react.dev/blog/2024/04/25/react-19#actions

*6:A common use case in React apps is to perform a data mutation and then update state in response https://react.dev/blog/2024/04/25/react-19#actions

*7:"A status object with the following properties:"