0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

こんにちは、H×Hのセンリツ大好きエンジニアです。(同担OKです😉)
Next.jsのプロジェクトにおいて、 UIとロジックを同一ファイルに書くことがかなり多く見られます。
勿論、同一ファイルとすることのメリットもあるとは思いますが、

責務は出来る限り分けたい!!!🥹

ということで、早速UI/ロジックを分離させましょう。
(こちらの記事を参考にしました。分かりやすい記事をありがとうございます。🙇)

UI/ロジックの分離

まず、UI/ロジックを切り離すメリットとしては以下のものが考えられます。

  • 関心事を分離できるので何をしているか分かりやすい
  • テストしやすい
  • 変更しやすい

これらを実現するために、分離させていきます。

ディレクトリ構成

page.tsxというファイルを、以下のように分離します。

.
├── page.tsx      <- UIを記述
└── page.hooks.ts <- ロジックを記述

上記のように、page.tsxでUI、page.hooks.tsでロジックを管理します。

UI/ロジックを分離する対象のファイル

以下の記事でも紹介した、無限スクロールを実装した記事一覧ページを分離していきます。

page.tsx
"use client"

import { ArticleProps } from "@/components/molecules/ArticleCard/ArticleCard"
import { CONST, STATUS_CODE } from "@/const"
import { useCallback, useEffect, useRef, useState } from "react"
import { Loading } from '@/components/molecules/Loading'
import { NoArticle } from '@/components/atoms/NoArticle'
import { ArticleList } from '@/components/organisms/ArticleList'
import { Box, Container, Text } from '@chakra-ui/react'

export const Page = () => {
  const [ offset, setOffset ] = useState(1)
  const [ articles, setArticles ] = useState<ArticleProps[]>([])
  const loader = useRef<HTMLDivElement | null>(null)
  const [ isVisible, setIsVisible ] = useState(true)

  const handleObserver = useCallback((entities: IntersectionObserverEntry[]) => {
    const target = entities[0]
    if (target.isIntersecting) {
      setOffset((prev) => prev + 1)
    }
  }, [])

  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api${CONST.ARTICLES}?per_page=${offset}`)
      if (response.ok) {
        const result = await response.json()
        switch (result.status) {
          case STATUS_CODE.OK:
            if (result.data.length === 0) {
              setIsVisible(false)
            }
            setArticles((prev) => [...prev, ...result.data])
            break // 成功時の処理が完了したらbreakを忘れずに
          default:
            alert(result.status)
            break
        }
      } else {
        alert(response.statusText);
      }
    }
    fetchData()
  }, [offset])

  useEffect(() => {
    const observer = new IntersectionObserver(handleObserver, {
      root: null,
      rootMargin: "20px",
      threshold: 0.5
    });
    if (loader.current) observer.observe(loader.current);

    return () => {
      observer.disconnect();
    };
  }, [handleObserver]);

  return (
    <>
      <Text fontSize="32px" fontWeight={600} lineHeight={1.8} mt="30px" textAlign="center">
        新着記事一覧
      </Text>
      <Container maxW="container.md" py="5%">
        <ArticleList articles={articles} />
        {!isVisible && articles.length === 0 && <NoArticle />}
        <Box ref={loader} h="1px" mt="19px" />
        {isVisible && <Loading />}
      </Container>
    </>
  )
}

簡単に内容を説明すると、

page.tsx
const handleObserver = useCallback((entities: IntersectionObserverEntry[]) => {
  const target = entities[0]
  if (target.isIntersecting) {
    setOffset((prev) => prev + 1)
  }
}, [])

監視対象のDOM要素がビューポート内に表示された場合にoffsetを増加させて、次のデータをフェッチするためのトリガーにする関数。

page.tsx
useEffect(() => {
  const fetchData = async () => {
    const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api${CONST.ARTICLES}?per_page=${offset}`)
    if (response.ok) {
      const result = await response.json()
      switch (result.status) {
        case STATUS_CODE.OK:
          if (result.data.length === 0) {
            setIsVisible(false)
          }
          setArticles((prev) => [...prev, ...result.data])
          break // 成功時の処理が完了したらbreakを忘れずに
        default:
          alert(result.status)
          break
      }
    } else {
      alert(response.statusText);
    }
  }
  fetchData()
}, [offset])

offsetが変更されるたびに記事データを取得する非同期関数を定義し、APIからデータをフェッチして記事の配列を更新する関数

page.tsx
useEffect(() => {
  const observer = new IntersectionObserver(handleObserver, {
    root: null,
    rootMargin: "20px",
    threshold: 0.5
  });
  if (loader.current) observer.observe(loader.current);

  return () => {
    observer.disconnect();
  };
}, [handleObserver]);

監視対象のDOM要素がビューポートに表示されるタイミングを検出するための設定を行うuseEffect

ロジックを分離

このファイルから、ロジックを分離させるとこのようになります。

page.hooks.ts
import { ArticleProps } from "@/components/molecules/ArticleCard/ArticleCard"
import { CONST, STATUS_CODE } from "@/const"
import { MutableRefObject, useCallback, useEffect, useRef, useState } from "react"

interface returnValue {
  articles: ArticleProps[]
  loader: MutableRefObject<HTMLDivElement | null>
  isVisible: boolean
}

export const usePageHooks = (): returnValue => {
  const [ offset, setOffset ] = useState(1)
  const [ articles, setArticles ] = useState<ArticleProps[]>([])
  const loader = useRef<HTMLDivElement | null>(null)
  const [ isVisible, setIsVisible ] = useState(true)

  const handleObserver = useCallback((entities: IntersectionObserverEntry[]) => {
    const target = entities[0]
    if (target.isIntersecting) {
      setOffset((prev) => prev + 1)
    }
  }, [])

  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api${CONST.ARTICLES}?per_page=${offset}`)
      if (response.ok) {
        const result = await response.json()
        switch (result.status) {
          case STATUS_CODE.OK:
            if (result.data.length === 0) {
              setIsVisible(false)
            }
            setArticles((prev) => [...prev, ...result.data])
            break // 成功時の処理が完了したらbreakを忘れずに
          default:
            alert(result.status)
            break
        }
      } else {
        alert(response.statusText);
      }
    }
    fetchData()
  }, [offset])

  useEffect(() => {
    const observer = new IntersectionObserver(handleObserver, {
      root: null,
      rootMargin: "20px",
      threshold: 0.5
    });
    if (loader.current) observer.observe(loader.current);

    return () => {
      observer.disconnect();
    };
  }, [handleObserver]);

  return { articles, loader, isVisible }
}

変更した点を説明します。

page.hooks.ts
interface returnValue {
  articles: ArticleProps[]
  loader: MutableRefObject<HTMLDivElement | null>
  isVisible: boolean
}

まずは、UIでの表示制御に必要な配列や関数などをreturnValueとして定義します。
返す値は以下のものです。

  • articles: 取得した記事の配列を管理
  • loader: DOM要素の参照を保持
  • isVisible: 無限スクロールが続くか示す
page.hooks.ts
export const usePageHooks = (): returnValue => {
.
.
.
  return { articles, loader, isVisible }
}

usePageHooksの返り値として、先ほど定義したreturnValueを使用しています。

UI制御に使うロジックの受け取り

UIを担当するpage.tsxの中身はこちらです。

page.tsx
'use client'

import { ArticleList } from '@/components/organisms/ArticleList'
import { Box, Container, Text } from '@chakra-ui/react'
import { usePageHooks } from './Page.hooks'
import { RequestCookie } from 'next/dist/compiled/@edge-runtime/cookies'
import { Loading } from '@/components/molecules/Loading'
import { NoArticle } from '@/components/atoms/NoArticle'

export const Page = () => {
  const { articles, loader, isVisible } = usePageHooks()
  return (
    <>
      <Text fontSize="32px" fontWeight={600} lineHeight={1.8} mt="30px" textAlign="center">
        新着記事一覧
      </Text>
      <Container maxW="container.md" py="5%">
        <ArticleList articles={articles} />
        {!isVisible && articles.length === 0 && <NoArticle />}
        <Box ref={loader} h="1px" mt="19px" />
        {isVisible && <Loading />}
      </Container>
    </>
  )
}

ここでは、usePageHooksからUIで使用するものを受け取っています。
このようにして、UIとロジックを分離させる事ができます!🥴

おわりに

今回はファイル毎に責務を分けるためにUI/ロジックを分離させてみました!
上記のようにUI/ロジックを分離させる事が出来るので、是非やってみてください!🤩

最後までご覧いただきありがとうございました!
以上、センリツでした。🤓

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?