こんにちは。ウェブアプリケーショングループのエンジニアの川井です。 今回はReactアプリケーションの再描画を抑えるために、画面をいくつかのパーツに分け、各パーツごとにルーティングを管理するようにした話を紹介します。
背景
Studyplus for SchoolではReactとReact Routerを利用してSPAを実装しています。Reactを使ってSPAを実装する際にはReact Routerを使ってルーティングを管理することが多いです。React Routerの主な機能としてはURLに応じて表示するコンポーネントを切り替えることです。以下のようにReact Routerが提供しているSwitchとRouteコンポーネントを使って表示するコンポーネントの切り替えができます。
<Switch> <Route path={"/home"}> <HomePage /> </Route> <Route path={"/information"}> <InformationPage /> </Route> <Route path={"/about"}> <AboutPage /> </Route> </Switch>
ここでは、ユーザーが/home
に遷移した際<HomePage />
のコンポーネントが表示され、/information
に遷移したら<InformationPage />
のコンポーネントが表示されます。
この方法を利用してクライアント側でページ遷移を管理すると遷移する際にサーバーから次のページのHTMLをもらわなくても良くなります。その他クライアント側でルートの管理をするメリットは、必要な部分だけ更新ができることです。以下のようにサイドバーのコンポーネントをページのメインコンテンツと別の<Switch>
に渡すとサイドバーとメインコンテンツを別々のルーティングにできます。
<Switch> <Route path={'/'}> <Sidebar /> </Route> </Switch> <Switch> <Route path={'/home'}> <HomePage /> </Route> <Route path={'/information'}> <InformationPage /> </Route> <Route path={'/about'}> <AboutPage /> </Route> </Switch>
この場合ユーザーがどのURLに遷移しても<Sidebar />
は再描画されません。(コンポーネント内でstateなどの変更があったら再描画されます)。
Studyplus for Schoolの画面では大きくヘッダー、サイドバー、メインコンテンツの3つの部分があります。この中でメインコンテンツが一番多く更新されます。 サイドバーとヘッダーの中身はそこまで変わることがないためメインコンテンツほど再描画する必要はありません。ただし、React-Routerの1つ目の例のように1つのURLに対して1つ大きいコンポーネントを実装していることが多かったため、再描画されてました。
こちらのようにメインコンテンツのタブを切り替えた際に全体のDOMが更新され、devtoolsで追加したスタイルも消えました。(タブ切り替え前にサイドバーとヘッダーに背景の色を追加しています。)この問題を解決するために、ルーティングを3つに分けようと決めました。そのため今まで実装していた大きいコンポーネントのリファクタリングをすることが必要でした。その中で現れた3つの課題を紹介します。
課題1サイドバーとヘッダーの切り出し
サイドバーやヘッダーのコンポーネントはメインコンテンツの中に置いていたため、コンポーネントをルーティングレベルで分ける修正が必要でした。メインコンテンツからコンポーネントを切り出すことで、サイドバーとヘッダーのレンダリングをメインコンテンツと分けることが可能になりました。
// Before <Switch> <Route path={"/about"}> <AboutPage /> </Route> </Switch>; const AboutPage = () => { return ( <div> <Sidebar /> <MainContent /> </div> ); };
// After <Switch> <Route path={'/'}> <Sidebar /> </Route> </Switch> <Switch> <Route path={'/about'}> <AboutPage /> </Route> </Switch> const Sidebar = () => ( <Sidebar /> ) const AboutPage = () => ( <MainContent /> )
課題2サイドバーがメインコンテンツから状態をもらっていた
サイドバーのコンポーネントには選択中のメニューアイテムをpropsで渡せるようになっており、今まではメインコンテンツのコンポーネントから渡してました。サイドバーをメインコンテンツのコンポーネントから切り出すため、propsとして渡すことができなくなったため、ルーティングのコンポーネントで管理するように修正しました。
// Before <Switch> <Route path={"/about"}> <AboutPage /> </Route> </Switch>; const AboutPage = () => { return ( <div> <Sidebar activeMenu="about" /> <MainContent /> </div> ); };
// After const routes = () => { const [activeMenu, setActiveMenu] = useState('') return ( <Switch> <Route path={'/'}> <Sidebar activeMenu={activeMenu}}/> </Route> </Switch> <Switch> <Route path={'/about'}> <AboutPage setActiveMenu={setActiveMenu}/> </Route> </Switch> ) } const Sidebar = () => ( <Sidebar /> ) const AboutPage = () => { useEffect(() => { props.setActiveMenu('about') }, []) return ( <MainContent /> ) }
課題3再描画に依存するロジック
ヘッダーのコンポーネント内ではある状態(通知の数など)を管理しており、以前はヘッダーのコンポーネントがレンダリングされた時にサーバーを問い合わせることで状態を更新していました。ヘッダーを切り出したことで、ページ遷移の際に再レンダリングされなくなり、状態が更新されなくなりました。こちらの部分は、URLの変更を検知してサーバーにリクエストをするように修正することで、うまく更新できるようになりました。
const Header = (props) => { const [headerData, setHeaderData] = useState(null); const getHeaderData() => { // サーバーに問い合わせてstateに入れる処理 } useEffect(() => { getHeaderData() }, [props.location.pathname]) return ( <Header headerData={headerData} /> ) }
まとめ
リファクタリングを終えた後また画面の再描画を確認するとメインコンテンツの中が更新されてもサイドバーやヘッダーが変わることはなくなりました。devtoolsで追加したスタイルがメインコンテンツを操作しても消えなくなりました。
ここではReact Routerを使って画面の再描画を最低限に抑える方法と改善を進めていた時の課題を3点紹介しました。是非こちらを参考に画面部分を分けて見てはいかがでしょうか。