はじめに
こんにちは、H×Hのセンリツ大好きエンジニアです。(同担OKです😉)
Next.jsのプロジェクトにおいて、 UIとロジックを同一ファイルに書くことがかなり多く見られます。
勿論、同一ファイルとすることのメリットもあるとは思いますが、
責務は出来る限り分けたい!!!🥹
ということで、早速UI/ロジックを分離させましょう。
(こちらの記事を参考にしました。分かりやすい記事をありがとうございます。🙇)
UI/ロジックの分離
まず、UI/ロジックを切り離すメリットとしては以下のものが考えられます。
- 関心事を分離できるので何をしているか分かりやすい
- テストしやすい
- 変更しやすい
これらを実現するために、分離させていきます。
ディレクトリ構成
page.tsx
というファイルを、以下のように分離します。
.
├── page.tsx <- UIを記述
└── page.hooks.ts <- ロジックを記述
上記のように、page.tsx
でUI、page.hooks.ts
でロジックを管理します。
UI/ロジックを分離する対象のファイル
以下の記事でも紹介した、無限スクロールを実装した記事一覧ページを分離していきます。
"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>
</>
)
}
簡単に内容を説明すると、
const handleObserver = useCallback((entities: IntersectionObserverEntry[]) => {
const target = entities[0]
if (target.isIntersecting) {
setOffset((prev) => prev + 1)
}
}, [])
監視対象のDOM要素がビューポート内に表示された場合にoffsetを増加させて、次のデータをフェッチするためのトリガーにする関数。
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からデータをフェッチして記事の配列を更新する関数
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
ロジックを分離
このファイルから、ロジックを分離させるとこのようになります。
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 }
}
変更した点を説明します。
interface returnValue {
articles: ArticleProps[]
loader: MutableRefObject<HTMLDivElement | null>
isVisible: boolean
}
まずは、UIでの表示制御に必要な配列や関数などをreturnValue
として定義します。
返す値は以下のものです。
-
articles
: 取得した記事の配列を管理 -
loader
: DOM要素の参照を保持 -
isVisible
: 無限スクロールが続くか示す
export const usePageHooks = (): returnValue => {
.
.
.
return { articles, loader, isVisible }
}
usePageHooks
の返り値として、先ほど定義したreturnValue
を使用しています。
UI制御に使うロジックの受け取り
UIを担当する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/ロジックを分離させる事が出来るので、是非やってみてください!🤩
最後までご覧いただきありがとうございました!
以上、センリツでした。🤓