しばしばユーザの権限ごとに画面を切り替えたい、という要望があります(まず間違いなくそれに伴って API 等のロジック面でも権限による機能の封鎖も行います)。この権限の分割の仕方は色々あるのですが、よく権限に関連する処理には URL 単位で権限に影響される部分がでてきます。例えば会員作成権限がない人には /admin/member/create という会員作成ページにそもそもアクセスさせない、といった具合です。この URL 単位の権限の挙動でよくあるのが権限外の機能へのリンクの非表示です。一見操作できるように見えて実は操作できない、というのはあまりよろしい挙動ではありませんのでそもそも非表示になるようにします。これを次の様にベタ書きすると、リンクの数だけ分岐が必要になります。
{canUseCompany && (
<Button>
<Link to={'/company'}>会社一覧</Link>
</Button>
)}
これは手間です。次の様にルーティングをまとめ、リンクをコンポーネントにすると記述を簡単にできます。
/**************
* Router.tsx *
**************/
type RouteDefine = {
path: string; // URL 定義
canUse: boolean; // この URL が使用可能か否か
};
// URL の代わりに key で個々のルーティング定義を呼び出す
type RouteKey = 'home' | 'companyIndex' | 'companyShow';
// ルーティング定義
export const useRouting = (): Record<RouteKey, RouteDefine> => {
// 権限を取得するフック
const permissions = usePermissions();
// 各ルーティングについて URL と権限定義を記述
return {
home: {
path: '/',
canUse: true,
},
companyIndex: {
path: '/company',
canUse: permissions.canUseCompany,
},
companyShow: {
path: '/company/:companyId',
canUse: permissions.canUseCompany,
},
};
};
/** ルーティングパラメータを置き換えて具体的に完成したパスにする */
export const makeRoutePath = (route: RouteDefine, params: { [key: string]: string | number } = {}): string => {
let returnPath = route.path;
Object.keys(params).forEach((key) => (returnPath = returnPath.replace(new RegExp(`:${key}`), String(params[key]))));
return returnPath;
};
/****************
* LinkBtn.tsx *
****************/
import Button, { ButtonProps } from '@material-ui/core/Button';
import { Link } from 'react-router-dom';
type LinkBtnProps = {
toRoute: RouteDefine;
params?: { [key: string]: string | number };
BtnProps?: ButtonProps;
};
/** ボタン形式のリンクコンポーネント */
export const LinkBtn: React.FC<LinkBtnProps> = (props) => {
const { toRoute, params, children, BtnProps } = props;
// props で渡って来たルーティング定義に付いている権限を解決する
if (!toRoute.canUse) {
// 権限がないなら非表示
return null;
}
// ボタンの見た目のアンカーリンクを返す
return (
<Button {...BtnProps} className={`link-btn ${BtnProps?.className || ''}`}>
<Link to={makeRoutePath(toRoute, params)}>{children}</Link>
</Button>
);
};
/**********
* 使用例 *
**********/
export const HogePage = () => {
const routing = useRouting();
return (
<div>
<LinkBtn toRoute={routing.companyIndex}>会社一覧</LinkBtn>
</div>
);
};
export const FugaPage = () => {
const routing = useRouting();
// ここに companyId を取得する何らかの処理
return (
<div>
<LinkBtn toRoute={routing.companyShow} params={{companyId}}>会社詳細</LinkBtn>
</div>
);
};
この様にリンクをコンポーネント化し、その中に表示、非表示切り替えロジックを組み込むことで分岐の記述回数を少なくできます。
デザインに一貫性がない場合、使えない手法ではありますがリンクに関する部分は大体これで行けます。この手法の副次効果としてルーティングに紐づく何かしらが増えた場合、都度 RouteDefine, useRouting にそれを追加することでルーティング定義が整理されたまま機能を拡張できる点があります。自分の場合、ページのタイトル、現在開いているページの URL と一致するかの判定メソッド、ルーティング先のコンポーネントに関係ないが遷移時に実行したい処理、などを記述することがあります。