Open7

個人開発でのNext.js(App Router)の気づき

gontagonta

server componentsにを活用しようとすると、コンポーネントの階層が自然と1階層増える

下記のようなドロップダウンのメニューから削除ボタンを押してリソースを削除するような機能があったとする。

ざっくり下記のような感じでコードを書いている。handleDeleteMovePostで投稿を消すことができるが、対象が投稿以外のコメントなどを削除できるようにしたいば、親がserver componentだとhandleDeleteCommentのようなイベントハンドラーを置くことができない。

'use client'
import { DeleteModal } from '@/components/common/DeleteModal'
import { usePopover } from '@/components/ui-parts/popover/CmPopover/hooks/usePopover'
import { api } from '@/trpc/react'
import { ChevronDownIcon } from '@heroicons/react/24/outline'
import { useRouter } from 'next/navigation'
import { type FC, type FormEvent, useState } from 'react'
import toast from 'react-hot-toast'
import { DeleteMenuPopover } from '../DeleteMenuPopover'

type Props = {
  movePostName: string
  movePostId: number
}

export const MovePostMenuIcon: FC<Props> = ({ movePostName, movePostId }) => {
  const { isOpen, refs, floatingStyles, setIsOpenPopover, getReferenceProps, floatingProps } = usePopover('bottom-end')
  const [isOpenDeleteModal, setIsOpen] = useState(false)
  const closeDeleteModal = () => {
    setIsOpen(false)
  }

  const handleOpenDeleteModal = () => {
    setIsOpenPopover(false)
    setIsOpen(true)
  }

  const deleteMovePostMutation = api.movePost.destroy.useMutation()
  const router = useRouter()

  const handleDeleteMovePost = async (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    await deleteMovePostMutation.mutateAsync({ id: movePostId })
    toast.success('投稿を削除しました。')
    router.push('/')
    router.refresh()
  }

  return (
    <>
      <button
        type="button"
        ref={refs.setReference}
        onClick={() => setIsOpenPopover((prev) => !prev)}
        {...getReferenceProps()}
      >
        <ChevronDownIcon width={22} />
      </button>
      {isOpen && (
        <DeleteMenuPopover
          {...{ floatingStyles, setFloating: refs.setFloating, setIsOpenPopover, floatingProps, handleOpenDeleteModal }}
        />
      )}

      <DeleteModal
        {...{
          target: `${movePostName}」の投稿`,
          isOpen: isOpenDeleteModal,
          closeModal: closeDeleteModal,
          onSubmit: handleDeleteMovePost,
        }}
      />
    </>
  )
}

gontagonta

server actionsを積極的に使おうとすると、formのactionを使って実装をする前提になるためやりづらいことがある。

共通のコンポーネントを作るときに、ここではform.submitをする、ここではイベントハンドラー経由でapiを叩きたいとなった時の共通化とかがやりづらいことがある。

gontagonta

コロケーションを意識したディレクトリにした時のimportパスの方法

基本的には絶対パスを使っているが、コロケーションを活用したディレクトリ構成箇所は相対パスで書くようにする。
このあたりをbiomeでlintでルールとかつけれるのだろうか??

gontagonta

ディレクトリ設計を考える。主にコンポーネント

希望として出来るだけpageに依存をしているものはapp/**/components配下におきたい。
ただ、秩序なく置きすぎると後で見直した時に混乱をする。どこの何があるかわからなくなる。
一定のルールを決めてできると良さそう。

ファイル名は出来るだけ階層やどこに何があるかがわかるような命名にする

トップページのものであれば

  • TopSearchFormの ような命名にする
    これをSearchFormのような命名にしたときに、他の画面でもSearchFormのような命名にしたときにエディタ上で検索した時にわかりづらくなる。
    何かに依存をしているコンポーネントであればそれが命名にくわわえた方がどこに依存をしているのかも分かり易い。

コロケーションを意識したディレクトリにした時のimportパスの方法

基本的には絶対パスを使っているが、app/**/componentsにコンポーネントを配置して、これに依存をした形でimportをするときは相対パスを使う。
逆に全く依存をしないようなところからimportをするときは絶対パスを使う

import { Button } from 'src/components/ui-parts/button/Button'

ui-partsについて

  • 命名は適当だが、ui-partsは共通化したものを置くのではなく、ライブラリーで提供しているような汎用コンポーネントだけを置く
  • 親が何であるかなどの関心は何も持たない。
  • Cmというプレフィックスはcommonの省略のプレフィックスとして使っているが、個人の好み、あとはプロジェクトによってはライブラリーを入れた時のButtonなどの命名がバッティングをしてしまい、asを使うことになり、複雑化をするのを防ぎたい。あとはCmってついてたらあのui-partsからimportをしているというのが一瞬でわかるので好き。
├── alert
│   └── CmAlert
│       ├── index.tsx
│       └── styles.module.css
├── button
│   ├── CmButton
│   │   ├── index.tsx
│   │   └── styles.module.css
│   ├── CmIconButton
│   │   ├── index.tsx
│   │   └── styles.module.css
├── card
│   ├── CmCard
│   │   ├── index.tsx
│   │   └── styles.module.css
│   └── CmMessageCard
│       ├── index.tsx
│       └── styles.module.css
├── checkbox
│   └── CmCheckbox
│       ├── index.tsx
│       └── styles.module.css
├── container
│   └── CmContainer
│       ├── index.tsx
│       └── styles.module.css
├── icon
│   ├── AddIcon.tsx
│   ├── ChevronRightIcon.tsx
│   └── SearchIcon.tsx
├── input
│   ├── CmIconInput
│   │   ├── index.tsx
│   │   └── styles.module.css
│   ├── CmInput
│   │   ├── index.tsx
│   │   └── styles.module.css
│   └── CmPasswordInput
│       ├── index.tsx
│       └── styles.module.css
├── label
│   └── CmLabel
│       ├── index.tsx
│       └── styles.module.css
├── layout
│   ├── CmDivider
│   │   ├── index.tsx
│   │   └── styles.module.css
│   ├── CmStackColumn
│   │   ├── index.tsx
│   │   └── styles.module.css
│   └── CmStackRow
│       ├── index.tsx
│       └── styles.module.css
├── link
│   └── CmIconLink
│       ├── index.tsx
│       └── styles.module.css
├── modal
│   └── CmModal
│       ├── index.tsx
│       └── styles.module.css
├── popover
│   └── CmPopover
│       ├── hooks
│       │   └── usePopover.tsx
│       ├── index.tsx
│       └── styles.module.css
├── radio
│   └── CmRadio
│       ├── index.tsx
│       └── styles.module.css
├── select
│   ├── CmLabelAndSelect
│   │   └── index.tsx
│   └── CmSelect
│       ├── index.tsx
│       └── styles.module.css
├── spinner
│   └── CmSpinner
│       ├── index.tsx
│       └── styles.module.css
├── tag
│   └── CmTag
│       ├── index.tsx
│       └── styles.module.css
├── text
│   ├── CmErrorText
│   │   ├── index.tsx
│   │   └── styles.module.css
│   └── CmHelpterText
│       ├── index.tsx
│       └── styles.module.css
├── textarea
│   └── CmTextarea
│       ├── index.tsx
│       └── styles.module.css
└── video
    └── CmVideo
        ├── index.tsx
        └── styles.module.css
gontagonta

技術構成

フロントエンド

next.js(14.2.5)

バックエンド

next.js(app api) でtrpcを使う

DB

prisma(postgres)

メール

resend

test

  • unit
    • vitest
  • e2e
    • playwright
gontagonta

useEffect がプロジェクトで2つしか無くなった。

  • 出来るだけ使わない実装にしたことで変なバグが起きなくなった気がする。
  • コードを見た時に順番に実装を追えばいいのでシンプルになった。
gontagonta

biomeのversionをv1.8.3 => v1.9.3に上げるといくつかエラーが増えた

https://biomejs.dev/linter/rules/no-undeclared-dependencies/

server-onlyを宣言していてもこのエラーが出たり、するのでdisabledにして対処をする

https://biomejs.dev/linter/rules/use-import-extensions/

こちらのuseImportExtensionsのエラーも出るが既存実装で挙動的に問題なさそうなのと、拡張子を毎回気にするのも面倒なので、これもdisabledにして対処をする。

https://biomejs.dev/ja/linter/rules/no-react-specific-props/

今までエラーで出なかったがこれがlintのエラーで修正をされて、全ファイルのclassNameがclassに変わって少しびっくりする。
こちらReactを使うのであればdisabledにする