フロントエンドのDTOを加工するパターンを考える
Reactでドメインロジックに関係するデータを取り扱う時はAPIやDBの値をそのまま使うのではなく、フロントエンド用のDTOを作成しています。
フロントエンドのDTO
例えば投稿のDTOであればこんな感じ
1 2 3 4 5 6 7
interface PostDTO { ... title: string slug: string body: string publishedAt: number }
このDTOをコンポーネントのPropsで受け取ります。
1 2 3 4 5 6 7 8 9
interface Props { post: PostDTO } export function Post(props: Props) { const { post } = props // return postを使ったUI }
フロントエンドで専用のDTOを取り扱うのはかなりしっくりときていますが、実装のパターンとしてよく考えることがDTOの値を加工する必要が出た時にどうするか。
個人的な考えや主観は入っていますが、いつも自分がどういうことを考えてパターンの採用、不採用をしているか順番に見ていきます。
DTOを加工するパターン
ぱっと思いつくパターンは3つです。
コンポーネント化
まずはReactを使っているということでコンポーネント化するパターン。
1 2 3 4 5 6 7
export function PostPublishedAt(props: { post: PostDTO }) { const { post } = props return ( <>{dayjs.unix(post.publishedAt).format('YYYY年MM月DD日')}</> ) }
Reactを使っているなら見た目に関する加工はコンポーネントでやる、というのは自然でパターンとしては考慮できると思いますが、個人的にはこの方法はいつも採用していません。
理由は加工した値をコンポーネント以外で使うことができないからです。
例えばPropsとして加工した値を渡すような場合だと、コンポーネント化してあると結局は別でロジックをコピペのような形にせざる得ません。
汎用関数
次は汎用関数で値を取得するパターンです。
処理を関数として切り出すのはよくある手法で今回の場合においても、有効な方法だと思います。
1 2 3
export const formattedPublishedAt = (date: number) => { return dayjs.unix(date).format('YYYY年MM月DD日') }
汎用関数にするのは悪くない方法だし、プロジェクトによっては採用してもよいとは思いますがこちらの方法も個人的にはあまり使いません。
理由はDTOの加工という特定のドメイン要素の値を加工することが目的だからです。
汎用関数は1つ1つの関数が独立しているので、良くも悪くも結構好き勝手に実装ができてしまいます。
またまとまりという点でもファイルでグルーピングしているような形が、いまいち1つのDTOを加工するという役割と関係性が薄い印象です。
Hook化
Reactを使っているなら思いつくのがHookにするパターン。
DTOを引数にとって、加工した値を返す関数をまとめて定義する方法はかなりしっくりきます。
1 2 3 4 5 6 7 8 9
const useDecoratedPost = (dto: PostDTO) => { const formattedPublishedAt = () => { return dayjs.unix(dto.publishedAt).format('YYYY年MM月DD日') } return { formattedPublishedAt, } }
ですが特定の状況で使えない場合があり採用は悩ましいところ。
その状況はESLintを使っていて特定の設定を行っている場合です。
use◯◯の関数は読み込む場所によっては
1
React Hook "useState" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook function
というESLintのエラーが出ます。
これではReactコンポーネントでしか使うことができません。
コンポーネント化のところで見たようにコンポーネント以外で使いたい場合に都合が悪いです。
DTOの加工をいい感じにするには?
これまで見てきたパターンはどれも微妙に惜しい感じがします。
特にHook系はかなり良いパターンにも思えます。
そこでuse◯◯の関数ではなクラス化することでDTOの加工をすることにしました。
DTOのDecoratorクラスを作る
考え方としてはHookと同じですが、DTOを加工するようのクラスを作ります。
インスタンスを作る時の引数をDTOにすることで、加工した値もGetterで取得することができます。
1 2 3 4 5 6 7
class PostDecorator { constructor(private dto: PostDTO) {} get formattedPublishedAt() { return dayjs.unix(this.dto.publishedAt).format('YYYY年MM月DD日') } }
この手法で気になる点はReactコンポーネントがレンダリングされるたびにインスタンスが作り直されてしまう点です。
例えば下記のコードではPostItemが再レンダリングされるたびにインスタンスが作り直されます。
1 2 3 4
const PostItem = (props: { post: PostDTO }) => { const { post } = props const decoratedPost = new PostDecorator(post) }
もしもインスタンスが作り直されることでパフォーマンスに影響を与えるような場合には、useMemoでメモ化するなりも考慮が必要になります。
まとめ
これまでDTOを加工する方法として3つのパターンとDecoratorクラスを作る方法を見てきました。
結論としてはDecoratorクラスを作るパターンを推していますが、プロジェクトによっては汎用関数で定義するパターンや他の方法の方が適切な状況もあると思います。
DTOを加工するのにはどんなパターンがあるのか、そのパターンのメリットとデメリットを理解したうえで適切な設計をするのが一番の正解です。