実現したいこと
ReactRouterでページのルーティングを実装しているが、
ルーティングする度にサイトのヘッダー/フッター/サイドナビなど共通パーツまで
各ページで描画していて全然SPAではなかった
共通パーツはルーティングが走っても再描画されないようにしたい
2021/12追記
react-router-dom v6でこの辺の実装方法が変わったので追記
変更点は公式のv5->v6アップグレードガイドだったり
v6版チュートリアル/nested-routesを参考にすると理解しやすいです
環境
React v17.0.1
やったこと
はじめる前
index.js
ReactDOM.render(
<React.StrictMode>
<BrowserRouter>
<Switch>
<Route exact path="/aaa/page1">
<Page1 />
</Route>
<Route exact path="/bbb/page2">
<Page2 />
</Route>
<Route exact path="/bbb/page3">
<Page3 />
</Route>
</Switch>
</BrowserRouter>
</React.StrictMode>,
document.getElementById("root")
);
pages.tsx
export function Page1() {
return (
<>
<Header />
<p>Page1</p>
<Footer />
</>
);
}
export function Page2() {
return (
<>
<Header />
<SideNav />
<p>Page2</p>
<Footer />
</>
);
}
export function Page3() {
return (
<>
<Header />
<SideNav />
<p>Page3</p>
<Footer />
</>
);
}
上記のようなページを3つ持つサイトで以下の特徴を持ちます
- どのページもHeaderとFooterのコンポーネントを持つ
- /bbb/* のページ(Page2, Page3)は更に<SideNav>コンポーネントも共通で持つ
これらの共通的な3つのコンポーネントを再描画しないようなRouterを作っていきたい
形だけでもRouterをネストさせる
まず、形の上でもRouterをネストさせてみる
参考にするのは ReactRouterの公式Doc
exampleのコードを見ると<Switch>で分岐させた各Routeのchildren要素の中で更にを使ってRouteの分岐をさせている
真似して書いてみると以下のように
index.js
ReactDOM.render(
<React.StrictMode>
<BrowserRouter>
<Switch>
<Route exact path="/aaa/page1">
<Page1 />
</Route>
{/*今回変更した部分*/}
<Route exact path="/bbb">
<Switch>
<Route exact path="/bbb/page2">
<Page2 />
</Route>
<Route exact path="/bbb/page3">
<Page3 />
</Route>
</Switch>
</Route>
</Switch>
</BrowserRouter>
</React.StrictMode>,
document.getElementById("root")
);
まず /bbb
でRouteを分岐させることでPage2とPage3をまとめられた
まだ、共通化はしていないので動作としては変わっていない
共通部分をRouterの外に出す
Page2とPage3を比べると差分は<p>要素のみに見える(残りは共通)
そこでRouterを使って描画する部分を<p>要素のみにする
pages.tsx
export function Page1() {
return (
<>
<Header />
<p>Page1</p>
<Footer />
</>
);
}
export function Page2() {
return (
<>
<p>Page2</p>
</>
);
}
export function Page3() {
return (
<>
<p>Page3</p>
</>
);
}
そして共通化させたい<Header>/<Footer>/<SideNav>コンポーネントは
Page2とPage3を分岐させるRouterの上位に配置する
index.js
ReactDOM.render(
<React.StrictMode>
<BrowserRouter>
<Switch>
<Route exact path="/aaa/page1">
<Page1 />
</Route>
{/*今回変更した部分*/}
<Route exact path="/bbb">
<Header />
<SideNav />
<Switch>
<Route exact path="/bbb/page2">
<Page2 />
</Route>
<Route exact path="/bbb/page3">
<Page3 />
</Route>
</Switch>
<Footer />
</Route>
</Switch>
</BrowserRouter>
</React.StrictMode>,
document.getElementById("root")
);
これで /bbb
以下のページ遷移(Page2<>Page3)では共通化した
<Header>/<Footer>/<SideNav>コンポーネントは再描画されなくなった
最後に<Header>/<Footer>/コンポーネントはPage1でも共通なので更に共通化させる
index.js
ReactDOM.render(
<React.StrictMode>
<BrowserRouter>
<Header />
<Switch>
<Route exact path="/aaa/page1">
<Page1 />
</Route>
<Route exact path="/bbb">
<SideNav />
<Switch>
<Route exact path="/bbb/page2">
<Page2 />
</Route>
<Route exact path="/bbb/page3">
<Page3 />
</Route>
</Switch>
</Route>
</Switch>
<Footer />
</BrowserRouter>
</React.StrictMode>,
document.getElementById("root")
);
react-routerv6に対応する
上記記法はv5のもので2021/12現在最新のreact router v6では記法が大きく変わっています
今回の書き方に影響するものだけまとめると
- <Switch>を使わなくても<Route>をネストできるようになった
- ネストされた<Route>では親<Route>からの相対パスでpathを表現する
- <Route>に対応するコンポーネントはelement propsで記述する
- ネスト親のコンポーネント内にネスト子のコンポーネントを表現する<Outlet>コンポーネントが追加
詳細はv6版チュートリアル/nested-routesを見ていただくとしてindex.jsとpaes.tsxがどう変わるかだけ
index.js
ReactDOM.render(
<React.StrictMode>
<BrowserRouter>
<Route path="/" element={<PageFrame />}>
<Route path="aaa/page1" element={<Page1 />} />
<Route path="bbb/" element={<WithSideNav />}>
<Route path="page2" element={<Page2 />} />
<Route path="page3" element={<Page3 />} />
</Route>
</Route>
</BrowserRouter>
</React.StrictMode>,
document.getElementById("root")
);
pages.tsx
export function PageFrame() {
return (
<>
<Header />
<Outlet />
<Footer />
</>
);
}
export function WithSideNav() {
return (
<>
<SadeNav />
<Outlet />
</>
);
}
export function Page1() {
return (
<>
<Header />
<p>Page1</p>
<Footer />
</>
);
}
export function Page2() {
return (
<>
<p>Page2</p>
</>
);
}
export function Page3() {
return (
<>
<p>Page3</p>
</>
);
}
Routerのネストに必要な要素が少なくなり
記述コスト、視認性が圧倒的に良くなっている。 これが公式サポートの力...
各コンポーネント側もネスト子要素をchileden -> Outletと特別扱いすることで
より取り回しがしやすくなったのではないかと
完成
当初やりたかったことは実装できた
ほとんど公式チュートリアルにあった通りだが、実際に使ってみた方が理解しやすいなぁ
個人的にはRouterのchildren要素はスッキリさせたいので/bbb
以下のchildren要素をコンポーネント化するかと思う