Let's write β

プログラミング中にできたことか、思ったこととか

react-router-dom@5の未ログイン時リダイレクト用コンポーネント`PrivateRoute`をTypeScriptで書く

背景

Reactのアプリケーションを書く場面があり、ログイン系でありがちな

  • ログインしていないければ /login に返す
  • ログイン成功したらどこかに移動する

というパターンをやりたくなりました。

reactでのURLのパスとコンポーネントの対応をつけるときにはreact-router-domというのを使うのが定番なようですが、

www.npmjs.com

このreact-router-domでは、<Route>というタグを利用してルーティングを設定する事ができます。 また<Redirect>というタグを使う事でページ感のリダイレクトを実行させる事もできます

そのため、ログイン状態に応じてRedirectを返すか、コンポーネントのコンテンツ自体を表示するかの分岐を<Route>内のコンポーネントに記述してやれば 未ログイン時のリダイレクト処理は実現する事可能です。

しかし、各コンポーネントで毎度そのような記述をするのはボイラープレート的なコードが増えてしまいます。 react-router-domの公式ドキュメントに、そのような状況に対処するためのRouteタグのラッパー PrivateRouteのJavascriptでの作成例があるのですが、 今回のプロジェクトはTypeScriptで記述しているため、せっかくなのでTypeScriptで記述しておきたいところでした。

環境

  • react-router-dom: ^5.1.2

コード

import React from 'react'
import { Route, RouteProps, Redirect } from 'react-router-dom'
import AuthState from './AuthState';

interface IProps extends RouteProps {
    loginPath: string;
    authState: AuthState;
}

const PrivateRoute: React.FC<IProps> = props => {
    const { children: Children, component: Component, ...rest } = props;
    return (
        <Route
            {...rest}
            render={innerProps => {
                if (props.authState === AuthState.Unauthorized) {
                    return (<Redirect
                        to={{
                            pathname: props.loginPath,
                            state: { from: innerProps.location }
                        }}
                    />);
                } else if (props.authState === AuthState.Authorized) {
                    if (Children instanceof Function) {
                        return Children(innerProps);
                    } else if (Children != null) {
                        return Children;
                    } else if (Component != null) {
                        return React.createElement(Component, innerProps)
                    } else {
                        return null;
                    }
                }
            }
            }
        />
    );
}

export default PrivateRoute;

使い方

<PrivateRoute path="/test" loginPath="/login" authState={this.state.authState} component={Hello}/>

または

<PrivateRoute path="/test" loginPath="/login" authState={this.state.authState}>
  <Hello />
</PrivateRoute>

つまづいた所

children component renderの優先度

Routeタグでは props として 子タグとしてchildrencomponent 、そしてインラインレンダリングとしてrender関数など いくつかのプロパティでどのようなコンポーネントを表示するかを指定できますが

優先度として children > component > render となっておりchildrencomponentが指定されているとrenderは無視されてしまいます。 (これについてはconsoleを見ていると警告が出ます)

今回はRouteタグをラップして、Routeタグのrender関数でRedirectタグを吐きだすか、元々指定されたコンポーネントをレンダリングするかの分岐を実施したく、 また、Routeタグから簡単に置きかえられるように挙動はあまり変えたくありません。

そのため、まず一旦propsからchildrencomponentのプロパティを剥がしておいた上で、render関数の中で適切に優先度を保ったまま呼びだしてやる必要がありました。

参考

おまけ

ログイン成功してたらメインページへ

PrivateRouteの分岐を逆転させるだけでできます。

import React from 'react'
import { Route, RouteProps, Redirect } from 'react-router-dom'
import AuthState from './AuthState';

interface IProps extends RouteProps {
    mainPath: string;
    authState: AuthState;
}

const AuthenticateRoute: React.FC<IProps> = props => {
    const { children: Children, component: Component, ...rest } = props;
    return (
        <Route
            {...rest}
            render={innerProps => {
                if (props.authState === AuthState.Authorized) {
                    return (<Redirect
                        to={{
                            pathname: props.mainPath,
                            state: { from: innerProps.location }
                        }}
                    />);
                } else if (props.authState === AuthState.Unauthorized) {
                    if (Children instanceof Function) {
                        return Children(innerProps);
                    } else if (Children != null) {
                        return Children;
                    } else if (Component != null) {
                        return React.createElement(Component, innerProps)
                    } else {
                        return null;
                    }
                }
            }
            }
        />
    );
}

export default AuthenticateRoute;