タブがあるWebサイトで、別の画面に移動した後、元の画面にブラウザバックしたときに、最後に選択されていたタブに戻す方法についてです。
Reactの場合、知っている人は知っているのか、あまりこうゆう情報を見かけなかったのでまとめました。
なんのことか
Reactの場合、React Routerを使って画面遷移を作るのが一般的だと思うのですが、よく考えずそのまま作ると次のような画面の流れになってしまいます。
(見た目を整えてないのでわかりにくいですが)Tab 0, Tab 1, Tab 2で3つのタブが切り替えられる画面があったとして、
Tab 1を選んだ後、
次の画面(Homeに戻るを押す)に進んで、
ブラウザのメニューにある、戻る<で前の画面に戻ると、選択していたTab 1ではなくデフォルトのTab 0が表示される。
このとき最後に選択したタブに戻るようにしたい場合、どうやるかです。
どうするか
注意:BrowserRouterを使っている前提です。
①とりあえず最後に選択されたタブを戻したい場合
React Routerのあるメソッドとプロパティを組み合わせると、ちょっと変更しただけで実現できました。
- タブが切り替わった時に、history.replace()で今のヒストリー情報を変更する。そのとき、paramsにタブの選択番号をセットしておく。
- 画面を表示するときは、history.location.paramsにセットしておいたタブの選択番号を取り出して、該当のタブを表示するようにする。
実装例の抜粋です。全体は、最後に載せています。
class KeepEnd extends React.Component {
constructor(props) {
super();
// historyのstateからタブの選択状態を設定する
this.state = {
tabIndex: this.getTabIndex(props.location)
};
this.onSelectTabIndex = this.onSelectTabIndex.bind(this);
}
// historyのstateからタブの選択状態を取得する
getTabIndex(location) {
let tabIndex = location.state && location.state.tabIndex;
if (tabIndex === undefined) {
tabIndex = 0;
}
return tabIndex;
}
onSelectTabIndex(index) {
console.log('KeepEnd: ', index);
this.setState({ tabIndex: index });
// 現在のhistoryを新しい選択状態に置き換えてる
// ※ state等で他にパラメータがない場合の例。他にパラメータがある場合それらをコピーしつつ変更する。
this.props.history.replace({
pathname: this.props.history.location.pathname,
state: { tabIndex: index }
});
}
render() {
return (
<div>
<Tabs selectedIndex={this.state.tabIndex} onSelect={this.onSelectTabIndex}>
{/* 省略 */}
</div>
);
}
}
// Routerのpropsを直接使うためwithRouterで囲む
export default withRouter(KeepEnd);
こうすると、期待どおりに戻ったときにタブ1が表示されます。
画面A タブ0
↓ タブ1を選択
画面A タブ1☆
↓ 画面Bに進む
画面B
↓ 画面Aに戻る(ブラウザバック)
画面A タブ1☆
実は、さらにすごいことに、2周してから戻ったときも、それぞれ最後の選択タブが表示されるようになります。
画面A タブ0
↓ タブ1を選択
画面A タブ1☆
↓ 画面Bに進む
画面B
↓ リンクから画面Aに進む
画面A タブ0
↓ タブ2を選択
画面A タブ2★
↓ 画面Bに進む
画面B
↓ 画面Aに戻る(ブラウザバック)
画面A タブ2★
↓ 画面Bに戻る(ブラウザバック)
画面B
↓ 画面Aに戻る(ブラウザバック)
画面A タブ1☆
②タブの切り替えを全部戻せるようにしたい場合
①の場合、タブを切り替えた状態は戻らない(タブ0からタブ1を選んで、ブラウザバックしたときタブ0に戻らない)ので、さらにひと工夫してタブの切り替えをブラウザバックで戻れるようにします。
- タブが切り替わる時に、**history.push()で新しいヒストリーをスタックに追加する。**そのとき、paramsにタブの選択番号をセットしておく。
- 画面を表示するときは、history.location.paramsにセットしておいたタブの選択番号を取り出して、該当のタブを表示するようにする。
- historyが変わったときにhistoryのstateからタブの選択状態を設定するリスナーをセットしておく(こうしないと、タブをクリックしても、同一コンポーネントのため中身が切り替わらない)。
class KeepAll extends React.Component {
constructor(props) {
super();
// historyのstateからタブの選択状態を設定する
this.state = {
tabIndex: this.getTabIndex(props.location)
};
this.onSelectTabIndex = this.onSelectTabIndex.bind(this);
// historyが変わったときにhistoryのstateからタブの選択状態を設定するリスナーをセットする
this.unlisten = props.history.listen((location) => {
this.setState({ tabIndex: this.getTabIndex(location) });
});
}
// componentWillUnmountでリスナーを解除する
componentWillUnmount() {
this.unlisten();
}
getTabIndex(location) {
let tabIndex = location.state && location.state.tabIndex;
if (tabIndex === undefined) {
tabIndex = 0;
}
return tabIndex;
}
onSelectTabIndex(index) {
console.log('KeepAll: ', index);
// historyに新しい選択状態でプッシュする
// ※ state等で他にパラメータがない場合の例。他にパラメータがある場合それらをコピーしつプッシュする
// ※ ここでは、stateのtabIndexは変更せず、上記のリスナーでtabIndexを更新する
this.props.history.push({
pathname: this.props.history.location.pathname,
state: { tabIndex: index }
});
}
render() {
return (
<div>
<Tabs selectedIndex={this.state.tabIndex} onSelect={this.onSelectTabIndex}>
{/* 省略 */}
</div>
);
}
}
// Routerのpropsを直接使うためwithRouterで囲む
export default withRouter(KeepAll);
これでよくあるWebサイトぽい挙動になります。
画面A タブ0
↓ タブ1を選択
画面A タブ1☆
↓ タブ2を選択
画面A タブ2★
↓ 画面Bに進む
画面B
↓ 画面Aに戻る(ブラウザバック)
画面A タブ2★
↓ ブラウザバック
画面A タブ1☆
↓ ブラウザバック
画面A タブ0
なぞ
インターネット場でよく見かける情報だと、Linkのstateにパラメータを設定して遷移させれば、遷移先のコンポーネントでパラメータが使えるよ的な紹介はよく見かけます。
ただ、なぜかブラウザバックと絡めた話はあまりなく、React Routerの公式のリファレンスでも、あっさりとした説明がされています。個人的に、単にパラメータを渡すだけでないすごい機能な気がしますが、ブラウザバックについては触れてないのが謎です。
しかもブラウザバックだけでなく、履歴を進めた時もそれらしい動きをしてくれるのですが。
state - (object) location-specific state that was provided to e.g. push(path, state) when this location was pushed onto the stack. Only available in browser and memory history.
push(path, [state]) - (function) Pushes a new entry onto the history stack
replace(path, [state]) - (function) Replaces the current entry on the history stack
state: State to persist to the location.
実装サンプル
動作確認される場合は、Create React Appでテンプレートを作って、react-routerを追加した後下記のファイルを入れ替えてください。
App.js: ルーティングをさせているところ。
import React from 'react';
import {
BrowserRouter as Router,
Switch,
Route,
Redirect,
} from 'react-router-dom';
import Home from './Home';
import NoKeep from './NoKeep';
import KeepEnd from './KeepEnd';
import KeepAll from './KeepAll';
function App() {
return (
<div className="App">
<Router>
<Switch>
<Route exact path="/">
<Home />
</Route>
<Route path="/nokeep">
<NoKeep />
</Route>
<Route path="/keepend">
<KeepEnd />
</Route>
<Route path="/keepall">
<KeepAll />
</Route>
<Route path="*">
<Redirect to="/" />
</Route>
</Switch>
</Router>
</div>
);
}
export default App;
Home.js: ホーム画面。単に3つのサンプルにいくためのホーム画面。
import React from 'react';
import {
Link
} from 'react-router-dom';
function Home() {
return (
<div>
<nav>
<ul>
<li>
<Link to="/nokeep">NoKeep</Link>
</li>
<li>
<Link to="/keepend">KeepEnd</Link>
</li>
<li>
<Link to="/keepall">KeepAll</Link>
</li>
</ul>
</nav>
</div>
);
}
export default Home;
NoKeep.js: 単純にタブを配置しただけで、ブラウザバック時にデフォルトに戻るパターン。
import React from 'react';
import {
Tab,
Tabs,
TabList,
TabPanel
} from 'react-tabs';
import {
Link
} from 'react-router-dom';
class NoKeep extends React.Component {
constructor() {
super();
this.state = {
tabIndex: 0
};
this.onSelectTabIndex = this.onSelectTabIndex.bind(this);
}
onSelectTabIndex(index) {
console.log('NoKeep: ', index);
this.setState({ tabIndex: index });
}
render() {
return (
<div>
<h1>NoKeep</h1>
<hr />
<Tabs selectedIndex={this.state.tabIndex} onSelect={this.onSelectTabIndex}>
<TabList>
<Tab>Tab 0</Tab>
<Tab>Tab 1</Tab>
<Tab>Tab 2</Tab>
</TabList>
<hr />
<TabPanel>
<h2>タブ 0 の内容</h2>
</TabPanel>
<TabPanel>
<h2>タブ 1 の内容</h2>
</TabPanel>
<TabPanel>
<h2>タブ 2 の内容</h2>
</TabPanel>
</Tabs>
<hr />
<Link to="/">Homeに戻る</Link>
</div>
);
}
}
export default NoKeep;
KeepEnd.js: ①とりあえず最後に選択されたタブを戻したい場合 のパターン。
import React from 'react';
import {
Tab,
Tabs,
TabList,
TabPanel
} from 'react-tabs';
import {
Link,
withRouter
} from 'react-router-dom';
class KeepEnd extends React.Component {
constructor(props) {
super();
// historyのstateからタブの選択状態を設定する
this.state = {
tabIndex: this.getTabIndex(props.location)
};
this.onSelectTabIndex = this.onSelectTabIndex.bind(this);
}
// historyのstateからタブの選択状態を取得する
getTabIndex(location) {
let tabIndex = location.state && location.state.tabIndex;
if (tabIndex === undefined) {
tabIndex = 0;
}
return tabIndex;
}
onSelectTabIndex(index) {
console.log('KeepEnd: ', index);
this.setState({ tabIndex: index });
// 現在のhistoryを新しい選択状態に置き換えてる
// ※ state等で他にパラメータがない場合の例。他にパラメータがある場合それらをコピーしつつ変更する。
this.props.history.replace({
pathname: this.props.history.location.pathname,
state: { tabIndex: index }
});
}
render() {
return (
<div>
<h1>KeepEnd</h1>
<hr />
<Tabs selectedIndex={this.state.tabIndex} onSelect={this.onSelectTabIndex}>
<TabList>
<Tab>Tab 0</Tab>
<Tab>Tab 1</Tab>
<Tab>Tab 2</Tab>
</TabList>
<hr />
<TabPanel>
<h2>タブ 0 の内容</h2>
</TabPanel>
<TabPanel>
<h2>タブ 1 の内容</h2>
</TabPanel>
<TabPanel>
<h2>タブ 2 の内容</h2>
</TabPanel>
</Tabs>
<hr />
<Link to="/">Homeに戻る</Link>
</div>
);
}
}
// Routerのpropsを直接使うためwithRouterで囲む
export default withRouter(KeepEnd);
KeepAll.js: ②タブの切り替えを全部戻せるようにしたい場合 のパターン。
import React from 'react';
import {
Tab,
Tabs,
TabList,
TabPanel
} from 'react-tabs';
import {
Link,
withRouter
} from 'react-router-dom';
class KeepAll extends React.Component {
constructor(props) {
super();
// historyのstateからタブの選択状態を設定する
this.state = {
tabIndex: this.getTabIndex(props.location)
};
this.onSelectTabIndex = this.onSelectTabIndex.bind(this);
// historyが変わったときにhistoryのstateからタブの選択状態を設定するリスナーをセットする
this.unlisten = props.history.listen((location) => {
this.setState({ tabIndex: this.getTabIndex(location) });
});
}
// componentWillUnmountでリスナーを解除する
componentWillUnmount() {
this.unlisten();
}
getTabIndex(location) {
let tabIndex = location.state && location.state.tabIndex;
if (tabIndex === undefined) {
tabIndex = 0;
}
return tabIndex;
}
onSelectTabIndex(index) {
console.log('KeepAll: ', index);
// historyに新しい選択状態でプッシュする
// ※ state等で他にパラメータがない場合の例。他にパラメータがある場合それらをコピーしつプッシュする
// ※ ここでは、stateのtabIndexは変更せず、上記のリスナーでtabIndexを更新する
this.props.history.push({
pathname: this.props.history.location.pathname,
state: { tabIndex: index }
});
}
render() {
return (
<div>
<h1>KeepAll</h1>
<hr />
<Tabs selectedIndex={this.state.tabIndex} onSelect={this.onSelectTabIndex}>
<TabList>
<Tab>Tab 0</Tab>
<Tab>Tab 1</Tab>
<Tab>Tab 2</Tab>
</TabList>
<hr />
<TabPanel>
<h2>タブ 0 の内容</h2>
</TabPanel>
<TabPanel>
<h2>タブ 1 の内容</h2>
</TabPanel>
<TabPanel>
<h2>タブ 2 の内容</h2>
</TabPanel>
</Tabs>
<hr />
<Link to="/">Homeに戻る</Link>
</div>
);
}
}
// Routerのpropsを直接使うためwithRouterで囲む
export default withRouter(KeepAll);
バージョン情報
バージョンで挙動が変わるかもしれないので、確認した時のバージョン情報。
- react: 16.12.0
- react-router: 5.1.2