React Conf 2018のKeynoteで発表されたHooks、Concurrent Reactのまとめ

フロントエンドエキスパートチームの@koba04です。

10/25,26の2日間、ネバダで開催されたReact Conf 2018に参加して来ました。 今回は、気になっている人も多いKeynoteで発表されたHooksとConcurrent Reactについて紹介したいと思います。

今回紹介された内容は、2014年後半くらいからReactを見てきた中でも、最も大きな変更であると言えます。

React Conf 2018
React Conf 2018

React Conf 2018のストラップ
React Conf 2018のストラップ

カンファレンスのトーク自体はすでにYouTubeで公開されているので、全トーク観ることが出来ます。

https://www.youtube.com/playlist?list=PLPxbbTqCLbGE5AihOSExAa4wUM-P42EIJ

Hooks

Hooksは、GitHub上でも一切公開されておらず、React Conf 2018のタイミングで初めて公開された機能です。 これまでも、Stateful Function Componentという構想は語られていたので、それを実現するAPIと言えます。

トークの動画は下記です。 デモ主体でわかりやすいので、是非トークを観ることをオススメします。

また、こちらのトークでは実際にHooksを使ったデモが行われました。

Hooksは、Function Componentに対する機能です。 現時点(2018/10/31)ではまだ提案段階の仕様ですが、16.7.0-alphaのバージョンで試すことが可能です。 下記のRFCのIssueにはすでに500件以上のコメントがある通り、注目度と議論を招くAPIであることがわかります。

https://github.com/reactjs/rfcs/pull/68

まだRFCの段階にも関わらず、公式ドキュメントにも8つのセクションに渡ってHooksの解説が用意されています。

https://reactjs.org/docs/hooks-intro.html

したがって、今の段階でもこれを読んで16.7.0-alphaを使うことで実際に試してみることが可能です。

まだ提案の段階にも関わらずここまで丁寧にドキュメントが用意されているのは、Hooksの意図を正しく伝えたいということを示していると思います。

重要なポイント

現時点では、提案段階の仕様であり実験的に試してフィードバックをもらう段階です。そのため、今後APIが変更される可能性があるため、現時点でプロダクション環境で利用することは推奨されていません (Facebookではすでにプロダクションで使っているようですが)

Hooksを利用することで、これまでClass Componentでしか出来なかったことがFunction Componentでも可能になります。 ただし、Class Componentも現時点では引き続きサポートされるため、既存のClass ComponentをHooksを使って書き直す必要は少なくとも今の時点ではありません。

将来的には、Function Componentだけになることが予想されますが、それは近い将来の話ではありません。 Facebookでも50000以上のReact Componentがあり、それら全てをHooksを使ったものに書き直す予定はないと言っています。

Class ComponentとHooksを使ったFunction Componentは一緒に使うことが可能なので、 新しいコードではHooksを使うなど、段階的な導入が可能です。

Hooksとは

HooksとはFunction Componentに対して追加された新しいAPIです。現状下記のHooksのAPIが提供されています。

  • Basic Hooks
    • useState
    • useEffect
    • useContext
  • Additional Hooks
    • useReducer
    • useCallback
    • useMemo
    • useRef
    • useImperativeMethods
    • useMutationEffect
    • useLayoutEffect

色々とありますが、まずはBasic Hooksとして定義されている3つのHooksだけでも知っておくといいと思います。

全てのHooksはuseから始まります。 これは後述するCustom Hooksを作成する際の命名規則としても適用されます。

Hooksの使い方

それでは、それぞれのHooksについて簡単に使い方を示します。

useState

useStateは、ComponentのLocal Stateを利用するためのHookです。 下記のようにStateの初期値を渡すと、現在の値Stateを更新するための関数を配列で返します。 useStateを使うことで、Class Componentを使うことなくLocal Stateを作成できます。

import React, {useState} from 'react';

const Counter = props => {
    const [count, setCount] = useState(0);
    return (
        <div>
            <p>count is {count}</p>
            <button onClick={() => setCount(count + 1)}>++</button>
            <button onClick={() => setCount(count - 1)}>--</button>
        </div>
    );
}

Stateを更新するための関数は、オブジェクトを渡した場合にもsetStateのようにマージされません。置き換えられます。

useEffect

useEffectは、副作用のある処理を定義するHookです。 API呼び出しやイベントの購読・解除など、componentDidMountcomponentDidUpdateなどのライフサイクルメソッドで行なっていたような処理を定義出来ます。 ただし、API呼び出しに対しては後述するSuspenseが適しているケースも多いです。

import React, {useEffect} from 'react';

const Header = props => {
    // textが更新されるたびにdocument.titleを更新する
    useEffect(() => {
        document.title = props.text;
    }, [props.text]);
    return <header>{props.text}</header>;
}

useEffectは第一引数に副作用のある処理を定義します。 第一引数のみ定義すると、Function Componentの関数が呼ばれる度にuseEffectに渡したコールバック関数も呼ばれます。 これは、componentDidMountcomponentDidUpdateそれぞれで呼び出す場合と同様と考えることができます。

useEffectの第二引数には、配列を指定出来ます。 配列を渡した場合、配列のいずれかの要素が変更されていた場合のみ、第一引数のコールバック関数が呼ばれます。 つまり上記の場合、props.textの値が変わった場合のみ、document.titleが更新されます。 これは、componentDidUpdateでPropsの値をチェックして変更があった場合のみ処理を行なっていたケースで利用できます。

useEffectに空の配列を渡すと、常に変化がないものとしてComponentのマウント時のみに、第一引数のコールバック関数が呼ばれます。 これは、componentDidMountを利用していたようなケースに利用できます。

useEffectは、関数を戻り値として返すことができます。 戻り値として返した関数はFunction Componentがアンマウントされる場合に呼び出されます。 これはサブスクリプションの登録、解除を行いたい場合に便利です。 下記は、イベントハンドラーの登録、解除をuseEffectを使って行なう例です。

import React, {useEffect} from 'react';

const Resize = props => {
    useEffect(() => {
        const handler = () => {
            // ...
        };
        window.addEventListener('resize', handler);
        // イベントを解除する関数を返す
        return () => window.removeEventListener(handler);
    }, []);
    return props.children;
}

useEffectはClass ComponentでのcomponentDidMountcomponentDidUpdateとは違い、DOM更新処理後に非同期で呼ばれます。 そのため、componentDidMountcomponentDidUpdateがDOM更新後に同期的に呼ばれることを保証したい場合には、後述のuseLayoutEffectを使用します。

useContext

useContextは文字通り、Contextを利用するためのHookです。 React.createContextで作成されるオブジェクトを引数として渡します。 ConsumerのComponentを渡すわけではない点に注意してください。

import React, {useContext} from 'react';

const ThemeContext = React.createContext('normal');

const Button = props => {
    const theme = useContext(ThemeContext);
    return (
        <button className={`${theme}-btn`} onClick={props.onClick}>
            {props.text}
        </button>
    );
}

useReducer

useReducerは、reducerinitialStateを渡すと、statedispatch関数を返すHookです。 Reduxをイメージするとわかりやすいと思います。 第三引数として、最初に発行するActionをオブジェクトとして渡すことも可能です。

import React, {useReducer} from 'react';

const reducer = (state, action) => {
    switch (action.type) {
        case 'INCREMENT':
            return state + 1;
        case 'DECREMENT':
            return state - 1;
        default:
            return state;
    }
};

const Counter = () => {
    const [state, dispatch] = useReducer(reducer, 0);
    return (
        <div>
            <p>count is {state}</p>
            <button onClick={() => dispatch({type: 'INCREMENT'})}>++</button>
            <button onClick={() => dispatch({type: 'DECREMENT'})}>--</button>
        </div>
    );
};

useReducerで作成できるのはLocal Stateですが、Contextと組み合わせることで、下記のようにReduxのようなグローバルなStateを作ることも可能ではあります。

https://github.com/koba04/react-hooks-like-redux

余談ですが、Reduxに関してはすでにIssueで議論がある通り、react-reduxのconnectで行なっていたことをHooksのAPIとして提供することが予想されます。

https://github.com/reduxjs/react-redux/issues/1063

ちなみに、前述したuseStateuseReducerを使って実装されています。

https://github.com/facebook/react/blob/3db8b80e1501b161b213b0b5405590e4325a0414/packages/react-reconciler/src/ReactFiberHooks.js#L324-L332

useCallback

useCallbackは、少しわかりにくですが、メモ化された関数を返すHookです。 第二引数に配列として渡された値が変わった場合のみ、第一引数で渡したコールバック関数が再生成されます。 つまり、第二引数に配列として渡した値が変わらない限り、同じコールバックを取得できます。 これはPureComponentやReact.memoを使った場合など、子のComponentがPropsで渡された値を比較して最適化を行なっている場合に有効です。

import React, {useCallback} from 'react';

// React.memoを使った最適化
const Child = React.memo(props => (
    <div>
        <p>{props.name}({props.score})</p>
        <button onClick={props.onClick}>++</button>
    </div>
));

const Parent = () => {
    const [score, setScore] = useState(0);
    // scoreが変わった場合のみ再生成される
    const onClick = useCallback(() => {
        setScore(score + 1);
    }, [score]);
    // こう書くと毎回新しい関数が渡されてしまう
    // const onClick = () => setScore(score + 1);
    return <Child onClick={onClick} name="child" score={score} />;
};

useMemo

useMemoは、useCallbackと少し似ていますが、こちらはメモ化されたコールバックではなくを返すHookです。 第二引数に配列として渡された値が変わった場合のみ、第一引数で渡したコールバック関数を再評価して値を返します。 例えば、Propsとして渡された巨大なリストをフィルタリングするような、計算に時間のかかるケースで使うことが想定されます。

import React, {useMemo} from 'react';

const ItemList = props => {
    // itemsかtypeが変わった場合のみ再評価される
    const filteredItems = useMemo(() => (
        props.items.filter(item => item.type === props.type)
    ), [props.items, props.type])
    return (
        <ul>
            {filteredItems.map(item => (
                <Item key={item.id} item={item} />
            ))}
        </ul>
    );
};

useRef

useRefは、その名前の通りRefを格納するためのオブジェクトを作成することが主な用途のHookです。 ちなみにRef以外のオブジェクトも格納できます。

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

// マウント時にだけフォーカスをあてる
const Input = props => {
    const el = useRef(null);
    useEffect(() => {
        el.current.focus();
    }, [])
    return <input type="text" ref={el} {...props} />;
}

useImperativeMethods

useImperativeMethodsは、forwardRefを使ってRef経由で親からアクセスさせる際に、RefのオブジェクトをカスタマイズするためのHookです。 ユースケースとしては多くないと思います。

import React, {useImperativeMethods, useRef} from 'react';

const MyInput = forwardRef((props, ref) => {
    const el = useRef(null);
    useImperativeMethods(ref, () => ({
        focus: () => {
            el.current.focus();
        }
    }));
    return <input type="text" ref={el} {...props} />;
});
// ここで取得できるrefはfocusメソッドのみ持ったオブジェクト
// <MyInput ref={ref} />

useMutationEffect

useMutationEffectは、利用方法はuseEffectと同じですが、実行されるタイミングが異なるHookです。 useMutationEffectは、ReactがDOMを更新するのと同じタイミングで同期的に呼び出されます。 DOMが更新されるタイミングで処理をしたい場合に利用します。

DOM更新中に同期的に実行されるため、DOMのプロパティにアクセスすることで強制的なレイアウトの再計算が行われ、パフォーマンスに悪い影響を与える可能性があります。 基本的には利用することを避けるべきHookだと思います。

useLayoutEffect

useLayoutEffectは、useMutationEffectと似ていますが、こちらは全てのDOMの更新処理が終わったタイミングで同期的に呼ばれるHookです。 更新後のレイアウト情報をDOMから同期的に取得したい場合に利用します。 これは、Class ComponentのcomponentDidMountcomponentDidUpdateが呼ばれるタイミングと同じタイミングで呼ばれます。 DOMの更新処理の後に同期的に呼ばれるため、可能であればuseEffectを使う方が望ましいです。

useEffectuseMutationEffectuseLayoutEffectのタイミングをUser Timingで示すと下記の通りです。 useMutationEffectがHost Effects(DOMの更新処理のタイミング)に、useLayoutEffectがライフサイクルメソッド呼び出しのタイミングに、useEffectがそのあと非同期に呼ばれていることがわかります。

各Effectのタイミングの違い
各Effectのタイミングの違い

Hooksの制限

HooksはFunction Componentの関数内から呼び出す必要があります。 また、HooksはFunction Component内において、毎回同じ順番で呼び出す必要があります。 そのため、関数内のトップレベルで呼び出すことが推奨されています。条件分岐などの中で呼び出すことは避ける必要があります。

const Foo = props => {
    // OK
    const [foo, setFoo] = useState('foo');
    if (props.bar) {
        // NG
        const [bar, setBare] = useState('bar');
    }
    return null;
}

これをチェックするためのeslint-plugin-react-hooksというESLintのプラグインも同時に公開されています。

https://reactjs.org/docs/hooks-rules.html#eslint-plugin

この制限は、少しわかりにくく感じますが、HooksのAPIをシンプルにするためのトレードオフとして選択したとのことです。

Custom Hooksの作り方

Hooksは前述した通りFunction Componentから呼び出す必要がありますが、独自に定義したHooksの中で呼ぶことも可能です。 そのため、特定の処理をHooksとして共通化することが可能です。

Custom Hooksは前述したeslint-plugin-react-hooksでのチェックを有効にするためにも、useから始まる名前で作成することが推奨されています。 例えば、windowのサイズを返すHookは、下記のように作成して使用できます。 作成したHookは、Componentの描画には一切関与していないため、どこでも再利用できます。

  • useWindowSize.js
import {useState, useEffect} from 'react';

// windowサイズを返すHook
export const useWindowSize = () => {
    const [width, setWidth] = useState(window.innerWidth);
    const [height, setHeight] = useState(window.innerHeight);
    useEffect(() => {
        const handler = () => {
            setWidth(window.innerWidth);
            setHeight(window.innerHeight);
        }
        window.addEventListener('resize', handler);
        return () => window.addEventListener(handler);
    }, []);
    return [width, height];
}
  • 作成したHookを使う
import React from 'react';
import {useWindowSize} from './useWindowSize';

const WindowSize = () => {
    const [width, height] = useWindowSize();
    return <p>width:{width}, height:{height}</p>
};

Hooksの目的

Componentのロジックを再利用するための手段としては、Higher Order Component(以下HOC)やRender Propsのパターンがありますが、 これらはコードを理解するのが難しかったり、"wrapper hell"と呼ばれる大量のComponentのネストが作成されるという問題があります。 recomposeのようなHOCのユーティリティライブラリを使っていて、気づかないうちに"wrapper hell"を作ってしまっているケースも多いと思います。 "wrapper hell"はデバッグの難しさや、見た目のViewの構造以上にComponentが大量にネストすることで、パフォーマンスに対しても影響があります。

また、サーバーからのデータ取得やイベントの購読・解除の処理をClass Componentが持つライフサイクルメソッドを使って記述しようとすると、コードが冗長になったりライフサイクル毎に処理が分断してしまう問題があります。 加えて、全てのライフサイクルメソッドの挙動を理解して適切にロジックを書くのは難しいという意見もあります。

Hooksは、Componentのロジックの再利用をするためのPrimitiveとして新しく提案されました。

例えばHooksでは、Componentのライフサイクルについて考える必要はなく、useEffectなどを使うことで「この値が変わったらこの処理をする」といったように、行いたい処理だけに注目してロジックを書くことができます。

Reactでは、Function Componentを使ったとしても内部ではFiberと呼ばれているデータ構造で状態を保持しています。 ライフサイクルメソッドがComponentに対するハイレベルなAPIであったのに比べて、HooksはFiberの内部構造に対するPrimitiveなAPIとして考えることもできます。 それは、@dan_abramovがKeynoteの中で、Atom(原子)に対するElectron(電子)のように、HooksはReactを構成する各機能の要素であると言っていることからもわかります。

React内部の仕組みについては、builderscon 2018で話したので、興味あれば見てみてください。

https://speakerdeck.com/koba04/algorithms-in-react

余談ですが、Hooksのソースを見てみると、HooksはUpdateと同様に単方向のLinked Listとして実装されているのがわかります。

https://github.com/facebook/react/blob/3db8b80e1501b161b213b0b5405590e4325a0414/packages/react-reconciler/src/ReactFiberHooks.js#L52-L60

これを、Function Componentが処理される度に初期化して順番にアクセスしていくため、必ず同じ順番に呼び出す必要があります。 他にも、useReducerの実装を見てみると、更新処理は既存のsetStateの仕組みとは違い、別の更新キューを作って処理していることがわかったりと興味深い部分も多いです。

Keynoteでは、Hooksの導入の理由としてJavaScriptにおけるClassが、機械にとっても人間にとっても扱いが難しいものである点をあげています。 例えば、イベントハンドラー登録時のthisの扱いにハマる人が多かったり、constructorなどの定型コードによりコード量が多くなったり。 また、Function Componentで書いた後に状態やライフサイクルメソッドが必要になった際に大きくコードを書き換える必要があるなど、ClassがDeveloper Experienceに与える影響を指摘しています。

また、機械にとってもClassは最適化(プロパティ名やメソッド名のminifyや処理のインライン化など)が難しいという問題点を指摘しています。 この辺りは、Prepackで取り組んでいる結果としての結論なのかなと思います。 他にもHot Reloadingも難しいという問題もあるようです。

現状、getSnapshotBeforeUpdatecomponentDidCatchgetDerivedStateFromErrorなど、一部のライフサイクルメソッドに対応するHooksが提供されていませんが、これらもいずれ提供される予定とのことです。

将来的にはHooksを使うことで、Class Componentを廃止する流れになるかと思います。 ですが、それは近い将来の話ではないので、慌てず少しずつ試していくのがいいかなと思います。

Concurrent React

Hooksが話題になる中、次の日のKeynoteではConcurrent Reactについて発表されました。

トークの動画は下記です。 デモ主体でわかりやすいので、是非トークを観ることをオススメします。

また、こちらのトークでは実際にSuspenseを使ったデモが行われました。

Concurrent Reactは、これまでAsync Renderingと呼ばれていたもので、SuspenseとTime-slicingの2つの機能を指します。 今回は新しい何かが発表されたというより、現在の状況を改めて説明して、デモでどういったことが可能になるかを示すものでした。

Suspense

Suspenseはレンダリングを中断(Suspend)できる機能です。 Suspenseは、Promise(正確にはthenableなオブジェクト)をPrimitiveとして扱うため、APIからのデータ取得やComponentの動的な読み込みだけでなく、Promiseでラップすることで様々な非同期処理に対して適用できます。

Suspenseの仕組みについては、過去に何度か紹介しているので興味あれば参照してください。 スライドで紹介しているバージョンからAPIの名前は変わっていますが、基本的な仕組みやコンセプトは同じです。

本トークでは、React.lazyと組み合わせた動的なComponent読み込みが紹介されていました。

import React from 'react';

const LazyContent = React.lazy(() => import('./LazyContent'));

const App = () => (
    <main>
        <section>
            <p>Main</p>
            <React.Suspense fallback={"loading..."}>
                <LazyContent />
            </React.Suspense>
        </section>
    </main>

);

上記では、LazyContentのComponentを動的に読み込んで、読み込まれるまではloading...のメッセージを表示しています。 上記の場合、loading...のメッセージが即座に表示されますが、後述するReact.ConcurrentModeを使うことで指定秒経過後にローディングをメッセージを出すといったことも可能になります。

上記で使用している動的なimportはまだ提案段階の仕様ですが、webpackなどを使っている場合にはすでに利用可能です。 このようにReact.lazyを使うことで、webpackなどのツールのサポートは必要ですが、importの仕方を変えるだけで簡単に動的な読み込みが可能となります。

React.SuspenseのComponentは親の位置であればどこでも配置可能なため、ローディングで隠す範囲も簡単に指定できます。 また、例えば複数の非同期な依存関係がある場合に、それらの親にReact.Suspenseを定義することで、全ての非同期の依存が解決するまで、単一のローディングメッセージを出すといったことも可能です。 このようにSuspenseを使うことで、柔軟な非同期読み込みの制御が可能です。

APIリクエストについては、react-cacheというパッケージのunstable_createResourceを利用することが下記の通り記述できます。 ただし、react-cacheについてはまだStableではなく、キャッシュのInvalidationなど欠けている機能もあるのでまだ実際に利用できるレベルではないとしています。

import React from 'react';
import {unstable_createResource as createResource } from 'react-cache';

// Promiseを返す処理からリソースを作成する
const userResource = createResource(id => {
    return fetch(`/api/user/${id}`).then(res => res.json())
});

const User = props => {
    // リソースを読み込む
    // リソースがキャッシュされていない場合は、Promiseがthrowされるのでレンダリングが止まる
    const user = userResource.read(props.id);
    return <div>{user.name}</div>;
}

const App = () => (
    <React.Suspense fallback="loading...">
        <User id={1} />
    </React.Suspense>
);

上記では、指定したユーザのデータがキャッシュになければfetchを使ってAPIから取得してキャッシュに格納します。 その際、React.Suspenseに指定しているloading...のメッセージが表示されます。 その後、APIレスポンスを受け取るとレンダリングが再開されます。 再開時には、APIレスポンスのデータがキャッシュに格納されていてデータを同期的に取得できるので、Userが表示されます。

Suspenseの応用例として、"Moving To Suspense"のトークでは、React.Suspensefallbackに低解像度の画像を指定して、高解像度の画像を非同期にロードすることで、最初は低解像度の画像を表示してそのあと高解像度の画像に差し替えるといったデモも行われてました。

また、unstable_createResourceで作成したリソースはreadだけでなくpreloadというメソッドも持っていて、これを使うことで、事前にデータをキャッシュしておくことも可能です。

Suspenseについては、上記のような同期モードでの基本的な挙動については、すでにStableだとしていますが、react-cacheを使ったAPIデータの取得やReact.ConcurrentModeを使ったConcurrentModeについては、まだまだStableでないとしています。

ConcurrentModeはレンダリングを非同期にするためのモードです。 React.ConcurrentModeのComponentで囲むことで、その中はConcurrentModeになります。 また、ReactDOM.createRoot(domElement).render(ReactElement)という新しい方法でDOMをマウントすることで、全体をConcurrentModeにすることも可能です。

部分的に非同期レンダリングを導入したい場合にはReact.ConcurrentMode、アプリケーション全体を非同期レンダリングしたい場合にはReactDOM.createRootを使います。

ConcurrentModeでは、React.SuspensemaxDurationを指定できます。 maxDurationを指定することで、fallbackを表示するまでの時間を制御できます。 例えば、ネットワーク環境がよくてAPIリクエストが1秒以内に返ってくるような状況では、ローディングを表示せずにデータのロードを待って表示した方がスムーズです。 そういった場合に、maxDuration={1000}のように指定することで、1秒経過してからローディングを表示するといった制御が可能です。

Suspenseを利用することで、非同期な依存関係の制御が簡単に柔軟にできるようになります。

また、サーバーサイドレンダリング対応についても取り組んでいるようです。

Time-slicing

Time-slicingは、更新処理を優先度付け出来る機能です。 プライオリティをベースとした協調的マルチタスクにより、メインスレッドをブロックしない更新処理を可能にします。 これにはschedulerというパッケージを利用します。

Reactでは、clickinputtouchmoveなど、ユーザーがすぐにリアクションを期待するようなイベントに対しては、InteractiveUpdateとして高い優先度が割り当てられます。 ただし、巨大なスプレッドシートに対するフィルタリングなど、一度の更新処理が重い場合にはユーザー入力や他の更新をブロックしてしまいます。 この場合、ユーザーにとってはフィルタリングするためのテキストボックスはすぐに反映されて欲しくて入力もブロックして欲しくないですが、フィルタリングした結果の表示については、少しくらい遅れても問題ないことが多いです。

このような場合に、Time-slicingを使うことで、フィルタリングするためのテキストボックスへの更新処理は優先度高く反映して、フィルタリングした結果については優先度を下げて反映を遅らせることが可能となります。

import React, {useState, useMemo} from 'react';
// このバージョンはまだnpmにpublishされていない
import {scheduleCallback} from 'scheduler';

const App = props => {
    const [text, setText] = useState('');
    const [filterText, setFilterText] = useState('');

    const filteredItems = useMemo(() => (
        props.items.filter(item => item.indexOf(filterText) !== -1)
    ), [filterText, props.items]);

    return (
        <main>
            <Input
                value={text}
                onChange={value => {
                    setText(value);
                    // Filterする方の優先度は下げる
                    scheduleCallback(() => {
                        setFilterText(value);
                    })
                }}
            />
            <List items={filteredItems}  />
        </main>

    );
};

上記のサンプルは、使用しているschedulerscheduleCallbackがまだ公開されていないため動作しませんが、以前作成したデモがあるので、そちらを試してもらうと雰囲気が掴めるかなと思います。

また、ConcurrentModeでは、hiddenのPropsによるメインスレッドを邪魔しないプリレンダリングが可能です。 hiddenのPropsをDOM Componentに指定することで、その子要素はOffScreen Priorityという特殊な優先度で処理されます。 OffScreen Priorityはとても低い優先度として定義されているため、他の更新処理をブロックしません。

トークではタブUIの例が示されていましたが、ユーザーが表示するページを先に読み込んでおくことで高速なページ遷移を実現できます。 Suspenseと組み合わせることで、事前に非同期の依存関係をロードすることが可能になるため、さらに強力な仕組みとなります。

const Home = React.lazy(() => import('./Home'));
const User = React.lazy(() => import('./User'));
const Settings = React.lazy(() => import('./Settings'));

const App = props => (
    <main>
        <div hidden={props.page === "user"}>
            <User />
        </div>
        <div hidden={props.page === "settings"}>
            <Settings />
        </div>
        <div hidden={props.page !== "user" && props.page !== "settings"}>
            <Home />
        </div>
    </main>
);

上記では、最初に表示したページ以外も動的に読み込んでプリレンダリングしています。

まとめ

このように今回発表された内容は、Reactを使ったアプリケーションの作り方を変える大きなものでした。 ここからReduxなどの周辺ライブラリがこれらのAPIをどう使うかにもよりますが、時間をかけて今回紹介された内容を使った書き方に変わっていくと思います。 まだ安定版としてリリースされたという状況ではないので、盛り上がりに踊らされず少しずつ試していくのがよさそうです。

また、Keynoteでは、この他にも新しいProfilerについての紹介もあったのでそちらも注目です。

サイボウズのフロントエンドエキスパートチームでは、一緒にフロントエンド分野の問題解決に取り組んでくれる仲間を募集しています。