共通化したコンポーネントライブラリでリンクを実装するのに一工夫必要だった話
こんにちは、steshimaです。
ソーシャルPLUS のフロントエンドには下記2つのアプリが存在し、それぞれ異なるルーティングライブラリを使用しています。
- Social Login Manager(以下「SLM」)
- Next Router
- Message Manager(以下「MM」)
- React Router
また、上記のアプリ内から呼び出す共通のコンポーネントライブラリ(@socialplus/components
)が存在します。
今回、この @socialplus/components
内のコンポーネントの中でリンクを設定したい箇所がありましたが、一工夫必要だったのでそちらについて記事にしました。
リンクコンポーネントがアプリに依存している
元々、SLM と MM にはそれぞれリンクコンポーネントが実装されていました。
これらは MUI の Link
コンポーネントと Next Router や React Router が提供する Link
コンポーネントを組み合わせた自前のコンポーネントです。
それ自体はよくある設計だと思いますが、冒頭にも紹介した通り ソーシャルPLUS では歴史的経緯によりアプリ毎に採用されているルーティングライブラリが異なります。
そのため @socialplus/components
に、アプリ側にあったリンクコンポーネントと同様のもので、且つどのアプリからも使える汎用的なリンクコンポーネントを実装するのが難しく、アプリ側で実装しているリンクコンポーネントを @socialplus/components
へ渡し、共通コンポーネント内でそれを使う必要がありました[1]。
React Context を使ってアプリからコンポーネントを渡す
コンポーネントを渡すといってもリンクを設定したい箇所が多いため、 Props で都度リンクコンポーネントを渡すという作りだと少し辛いです。
そのため React Context を使ってアプリからリンクコンポーネントを渡し、@socialplus/components
内でそれを呼び出す設計にしています。
コードとしては下記のようなイメージです。
まず@socialplus/components
側にコンテキストを生成し、それを使った共通コンポーネント内で使用するリンクコンポーネントを定義します。
export interface LinkProps {
readonly href: string | UrlObject;
...
}
export const LinkComponentContext = createContext<React.FC<LinkProps> | null>(
null,
);
export const Link: React.FC<LinkProps> = forwardRef((props, ref) => {
const LinkComponent = useContext(LinkComponentContext);
if (!LinkComponent) {
throw new Error('LinkComponentContext Provider is not set');
}
return <LinkComponent {...props} ref={ref} />;
});
次に、コンテキストのプロバイダに渡すリンクコンポーネントを MM と SLM それぞれに定義します。
Props もアプリの実装に依存して型が微妙に違ったりなど差異があるので、ここで吸収して@socialplus/components
のリンクコンポーネントの Props に合わせます。
まずは SLM。
import { MuiNextLink } from './MuiNextLink';
// 元々アプリ側に存在していた MUI と Next Router を組み合わせたリンクコンポーネント
const Link = React.forwardRef<HTMLAnchorElement, Props>(
({ href, noExternalIcon, children, ...rest }, ref) => {
...
return (
// MuiNextLink の中身は MUI の公式サンプルにある MUI と Next Router を組み合わせたコンポーネントをそのまま持ってきたものです
// https://github.com/mui/material-ui/blob/f646bcc3c76dd3745cf8f5c7de8b29f33de7f7cc/examples/nextjs-with-typescript/src/Link.tsx
<MuiNextLink ref={ref} href={href} {...rest}>
{children}
...
</MuiNextLink>
);
},
);
import { LinkProps } from '@socialplus/components';
export const NextLink: React.FC<LinkProps> = forwardRef(
(
{ href, children, underline, ... },
ref,
) => {
// 型の絞り込みなど
...
return (
<Link
ref={ref}
href={href}
underline={underline}
...
>
{children}
</Link>
);
},
);
次に MM。
import { Link as MuiLink } from '@mui/material';
import {
Link as RouterLink,
} from 'react-router-dom';
// 元々アプリ側に存在していた MUI と React Router を組み合わせたリンクコンポーネント
const Link = React.forwardRef<HTMLAnchorElement, Props>(
(
{ to, color, underline, children, noExternalIcon, target, ...rest },
ref,
) => {
...
const props = isExternal ? { href: to } : { component: RouterLink, to };
return (
<MuiLink
ref={ref}
{...props}
target={target ?? (isExternal ? '_blank' : undefined)}
...
>
{children}
...
</MuiLink>
);
},
);
import { LinkProps } from '@socialplus/components';
export const ReactRouterLink: React.FC<LinkProps> = forwardRef(
(
{ href, children, underline, ... },
ref,
) => {
// 型の絞り込みなど
...
return (
<Link
ref={ref}
href={href}
underline={underline}
...
>
{children}
</Link>
);
},
);
これでそれぞれのアプリにリンクコンポーネントを用意したので、アプリ側でプロバイダを使用して@socialplus/components
にコンポーネントを受け渡すようにします。
例えば、SLM であれば下記の通り。
const App: React.FC = () => (
<LinkComponentContext.Provider value={NextLink}>
...
</LinkComponentContext.Provider>
);
あとは共通コンポーネント内でリンクを設定したい箇所に、初めに定義した Link
コンポーネントを import して使用するといった具合になります。
import { Link } from './Link';
export const CommonComponent: React.FC = (
<div>
...
<Link href="https://example.com/" />
</div>
)
おわりに
今回はコンポーネントを自前のライブラリに渡したいというケースでしたが、他にもアプリに依存した情報を自前のライブラリに渡す必要があった場合、ライブラリ側にグローバルステート管理ライブラリなどを使ってアプリに依存するデータの置き場所を1つ作っておき、アプリの初期化処理などで必要な情報をまとめて設定するという方法もありそうだなとぼんやり考えています。
ただ、現状は依存するものが少なかったり、Props で受け渡すだけでも十分だったりと、今はまだそこまで凝った設計を考えなくても困っていないという状態です。
-
ルーティングライブラリが提供している機能部分(
Link
コンポーネント)を抽象化すれば@socialplus/components
内に汎用的なリンクコンポーネントを実装できますが、元々アプリ側にリンクコンポーネントを実装していたなどの理由で抽象化が難しそうなため、今はコンポーネント単位で分けています。 ↩︎
Discussion