かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

React + TypeScriptでreact-bootstrapのNavとreact-routerの共存

react-routerで画面遷移のリンクといえばLinkタグかIndexLinkタグを使います。 でも、react-bootstrapのNavを使って画面の上部のタブみたいなのを作ろうとすると、Linkタグは使えません。

f:id:okazuki:20160125203921p:plain

そんな時は、プログラムからhistoryを制御してやるといいです。

こんな感じにRouteを定義して

var router = (
    <Router history={history.createHashHistory()}>
        <Route path='/' component={App}>
            <IndexRoute component={IndexPage} />
            <Route path='/page1'>
                <IndexRoute component={Page1} />
                <Route path=':id' component={Page1} />
            </Route>
        </Route>
    </Router>
);

ReactDOM.render(
    router,
    document.getElementById('content'));

Appクラスでは、こんな感じにNavbarを定義しておきます。

class App extends React.Component<React.Props<{}>, {}> {
    render() {
        return (
            <div>
                <Navbar>
                    <Navbar.Header>
                        <Navbar.Brand>
                            <span>Route app!</span>
                        </Navbar.Brand>
                    </Navbar.Header>
                    <Nav>
                        <NavItem href='/'>Index</NavItem>
                        <NavItem href='/page1'>Page1</NavItem>
                    </Nav>
                </Navbar>
                <Grid>
                    <Row>
                        <Col md={12}>
                            {this.props.children}
                        </Col>
                    </Row>
                </Grid>
            </div>
        );
    }
}

IndexPageとPage1は適当に定義しておきます。

// IndexPage
import * as React from 'react';
import {Link} from 'react-router';

export default class IndexPage extends React.Component<{}, {}> {
    render() {
        return (
            <Link to='/page1/10'>/page1/10</Link>
        );
    }
}
// Page1
import * as React from 'react';
import * as ReactRouter from 'react-router';

export default class Page1 extends React.Component<ReactRouter.RouteComponentProps<{}, { id: number }>, {}> {
    render() {
        return (
            <h1>Page1 {this.props.routeParams.id}</h1>
        );
    }
}

実行するとこんな感じになります。

f:id:okazuki:20160125210530p:plain

まだリンクをクリックしてもちゃんと動きません。クリックでちゃんと動くようにするにはNavタグのonSelectイベントをハンドリングする必要があります。このonSelectの第2引数にhrefが渡ってくるので、こいつをhistoryにpushしてやります。

class App extends React.Component<RouteComponentProps<{}, {}>, {}> {

    private handleSelect(key: number, href: string) {
        this.props.history.push(href);
    }

    render() {
        return (
            <div>
                <Navbar>
                    <Navbar.Header>
                        <Navbar.Brand>
                            <span>Route app!</span>
                        </Navbar.Brand>
                    </Navbar.Header>
                    <Nav onSelect={this.handleSelect.bind(this)}>
                        <NavItem href='/'>Index</NavItem>
                        <NavItem href='/page1'>Page1</NavItem>
                    </Nav>
                </Navbar>
                <Grid>
                    <Row>
                        <Col md={12}>
                            {this.props.children}
                        </Col>
                    </Row>
                </Grid>
            </div>
        );
    }
}

Page1に遷移した様子

f:id:okazuki:20160125210941p:plain

次に、カレントのページのタブの色を変えます。これはちょっとめんどくさくて、Stateに現在のカレントのhrefを持たせるプロパティを定義します。

interface AppState {
    activeHref: string;
}

class App extends React.Component<RouteComponentProps<{}, {}>, AppState> {
    // 省略
}

そして、componentDidMountでhistoryのlistenで画面遷移を監視して、そこでisActiveを使って現在のカレントのhrefを探し当てます。どんなページがあるのかということを予め定義しておいて、そこから探すのがお手軽です。ついでに、NavItemも、その情報をベースに組み立てるようにするといい感じになります。

const pageMap = [
    { href: '/', label: 'Index', indexOnly: true },
    { href: '/page1', label: 'Page1', indexOnly: false },
];

interface AppState {
    activeHref: string;
}

class App extends React.Component<RouteComponentProps<{}, {}>, AppState> {

    private hisotryToken: Function;

    constructor(props: RouteComponentProps<{}, {}>) {
        super(props);
        this.state = { activeHref: '/' };
    }

    private handleSelect(key: number, href: string) {
        this.props.history.push(href);
    }

    componentDidMount() {
        this.hisotryToken = this.props.history.listen(() => {
            var activeHref = '';
            pageMap.forEach(map => {
                if (this.props.history.isActive(map.href, null, map.indexOnly)) {
                    activeHref = map.href;
                }
            });
            this.setState({
                activeHref: activeHref
            } as AppState);
        });
    }

    componentWillUnmount() {
        this.hisotryToken();
    }

    render() {
        let navItems = pageMap.map(x => <NavItem href={x.href}>{x.label}</NavItem>);
        return (
            <div>
                <Navbar>
                    <Navbar.Header>
                        <Navbar.Brand>
                            <span>Route app!</span>
                        </Navbar.Brand>
                    </Navbar.Header>
                    <Nav activeHref={this.state.activeHref} onSelect={this.handleSelect.bind(this)}>
                        {navItems}
                    </Nav>
                </Navbar>
                <Grid>
                    <Row>
                        <Col md={12}>
                            {this.props.children}
                        </Col>
                    </Row>
                </Grid>
            </div>
        );
    }
}

これで、タブに色がつくようになります。isActiveのいいところは、/page1でも/page1/10でもいい感じにアクティブかどうか判定してくれる点です。indexOnly引数にtrueを設定すると/みたいなインデックスのURLをいい感じに判定してくれるようになります。

f:id:okazuki:20160125212356p:plain

ソースコード

コードの全体はGitHubに上げています。

github.com