Next.jsで利用するContainer とPresenter構成について

Next.jsで利用するContainer とPresenter構成について

ディレクトリの責務について

#

本業の方で、このディレクトリ構造でコードを書き始めたので、備忘録がてらアウトプットをしていきます。

参考にした記事はこちらです。図もあって非常にわかりやすかったです!


まず、全体像から説明していきます。

アプリケーションはシンプルな数字のカウントアプリをイメージしてください。

それぞれのファイルの責務の概要はこんな感じです。

src/pages/index.tsx

ここはルーティングの責務です。


src/components/Counter.page.tsx

ここはレイアウトコンポーネントでラップし、ブラウザ情報などmeta要素を付与するためのコンポーネントです。HTMLタグのmainタグ以外の部分の描画の責務を担っています。


src/components/Counter.container.tsx

ここはロジック部分の責務を担うコンポーネントです。


src/components/Counter.hooks.tsx

ここはhooksロジックのみを記述する部分です。「 containerも同じ責務では?」と思われるかもしれません。containerにはhooks以外のロジックも多々書く場合があり、カスタムフックだけでもまとめると可読性がよくなるので、containerに差し込んでいます。


src/components/Counter.presenter.tsx

ここはUIの描画のみを記述する部分です。

全体の責務としてはこのような感じです。

端的にいえば、page → container → presenterとひたすらprops downしていくイメージです。


各コンポーネントのコードについて

#

全体像がわかったところで、各コンポーネントのコードを見ていくとより一層、理解が深まり、イメージもしやすくなるのではないかと思います。

pages/index.tsx
Copied!
import CounterPage from "../components/Counter.page";

//SSGまたはSSRでデータを取得する場合のprops
type StaticProps = {
  tabTitle: string
}

export default function Home ({ tabTitle }: StaticProps)  {
  return (
    <CounterPage tabTitle={tabTitle} />
  )
}

NextPageTypeとあるように、このコンポーネントはNext.jsの基本機能であるルーティングのみを担っています。もし、SSGやSSRでデータフェッチングするならここに記述し、components配下のpage.tsxにデータをpropsで渡します。


components/Counter.page.tsx
Copied!
import { FC } from "react"
import CounterContainer from "./Counter.container"
import Layout from "./Layout"

type ProsType = {
  tabTitle: string
}

const CounterPage: FC<ProsType> = ({ tabTitle }) => {
  // NOTE: headerやfooterでコンテナをラップしたりするコンポーネント.
  return (
    <Layout tabTitle={tabTitle}>
      <CounterContainer tabTitle={tabTitle} />
    </Layout>
  )
}
export default CounterPage

Counter.page.tsxはHTMLタグのmainタグ以外の部分の描画を担っています。

ここではCounter.container.tsxをLayout.tsxコンポーネントでラップしています。

Layout.tsxは単に、HeadコンポーネントにSSRで取得したブラウザのタブの変数をセットしているだけの責務です。


Layout.tsxも載せておきます。

layout.tsx
Copied!
import { FC, ReactNode } from "react"
import Head from "../node_modules/next/head"

import styles from "../styles/Home.module.css"

type PropsType = {
  tabTitle: string
  children: ReactNode
}

const Layout: FC<PropsType> = ({ tabTitle, children }) => {
  // NOTE: main以外の描画に関わるものを記述するコンポーネント.
  return (
    <div>
      <Head>
        <title>{tabTitle}</title>
      </Head>
      <main className={styles.main}>
        {children}
      </main>
    </div>
  )
}
export default Layout

Counter.page.tsxには、そのほかに、例えば、このコンポーネントの中で、Counter.container.tsxをheaderコンポーネントでラップしたり、footerコンポーネントでラップしたりなどをしたりするとよいと思います。

components/Counter.container.tsx
Copied!
import { useCounter } from "./Counter.hooks"
import CounterPresenter from "./Counter.presenter"

type ProsType = {
  tabTitle: string
}

const CounterContainer = ({ tabTitle }) => {
  // NOTE: ロジックを渡すコンポーネント.
  const { count, increment, decrement } = useCounter();
  return (
    <CounterPresenter count={count} increment={increment} decrement={decrement} />
  )
}
export default CounterContainer

いよいよタイトルにもある通り、containerの登場です。長い道のりでしたね(^^;

ここはロジックの責務を主に担当しています。もう少し詳しくいうと、ロジックをpresenter.tsxにpropsで渡す役割です。


しかし、コードを見てみると、そこまでゴリゴリとロジック書いていないことがわかります。今回のアプリケーションではほとんどのロジックはカスタムフックで記述できるので、後述するhooks.tsxに任せています。本来なら、containerには、例えば、page.tsxからpropsで渡されたデータを整形したり、加工したりするロジックが書かれていたりします。


components/Counter.hooks.tsx
Copied!
import { useState } from "react"

// NOTE: ロジックの本体を記述。
export const useCounter = () => {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(prev => prev + 1);
  }

  const decrement = () => {
    setCount(prev => prev - 1);
  }

  return { count, increment, decrement }
}

今回のアプリケーションではロジックの本体がこのファイルに記述されています。

いわゆるカスタムフックと同じです。コードの中をみると、useStateの更新関数やincrement, decrementの関数がここに記述されています。今回のサンプルコードでは説明の都合上、書かれていませんが、本来なら、useCallbackを用いて、パフォーマンス改善措置をした方がよいですね。


components/Counter.presenter.tsx
Copied!
import { FC } from "react";

type PropsType = {
  count: number;
  increment: () => void;
  decrement: () => void;
}

const CounterPresenter: FC<PropsType> = ({ count, increment, decrement }) => {
  // NOTE: 表示に関わるものを記述する。ロジックは記述しない。
  return (
    <div>
      <h1>シンプルカウンター</h1>
      <br />
      <button type="button" onClick={decrement}>-</button>
      <span>count: {count}</span>
      <button type="button" onClick={increment}>+</button>
    </div>
    
  )
}
export default CounterPresenter

いよいよ最後です。ここがHTMLのmainタグの中のUIを担当する部分になります。

ここではロジックは全くかかず、親コンポーネントからpropsで渡された関数や変数を用いて表示をしていくのみの責務です。CSSを当てたりして、スタイリングする部分はこのファイルになります。


お疲れ様でした。これが、container / presenterのディレクトリ構造の一例です。本業のディレクトリ構造について、備忘録がてら書いてみました〜最後まで読んでくださり、ありがとうございました!



GitHub
修正をリクエストする
Post to X