Open15

React.js (Next.js) の世界を知る 🗺

maechanmaechan

はじめに

やりたいこと

オシゴトで利用することになった React.js (Next.js) を取り巻く世界を知るために、様々なことをやっていく記録です。この辺りの技術スタックが気になっている(全部やるとは言っていない)。

- React.js / Next.js
- TypeScript
- React Hooks
- GraphQL (Apollo)
- Flex Layout / Grid Layout
- Atomic Design
- Redux
- Redux ToolKit
- BFF (Express, NestJS ...)

つい最近、モバイル(iOS & Swift)の世界から Web の世界にやって来て完全に浦島たろう状態 𓆉

けっぱります!

maechanmaechan

React.js の素振り

React.js の大まかな世界観の把握は、公式にある実践的なチュートリアルを参考にした。

https://ja.reactjs.org/tutorial/tutorial.html

また、公式ドキュメントもとても親切に書かかれているので、そちらも合わせて。

https://ja.reactjs.org/docs/hello-world.html

公式さんアリガトウ。

チュートリアルに挑戦

react-tutorial

https://github.com/maecha/ReactJS/tree/main/react-tutorial

maechanmaechan

React Hooks API の素振り

React Fooks API の理解を深めるために、Web にあるサンプルを元に動かしてみる。

素振りで取り扱う React Hooks API

  • 基本のフック
    • useState
    • useEffect
    • useContext
  • 追加のフック
    • useReducer
    • useCallback
    • useMemo
    • useRef

参考記事

https://ja.reactjs.org/docs/hooks-reference.html
https://qiita.com/seira/items/f063e262b1d57d7e78b4

開発ブランチ

https://github.com/maecha/react-hooks-examples

Hooks API のメモ

useState

useState は、関数コンポーネントで state を管理(state の保持と更新)するための React フック。唯一の引数は state の初期値。

基本形

react.jsx
const [count, setCount] = useState(initialState)

useEffect

useEffect は、第1引数に渡された関数を描画の結果が画面に反映された後に動作させる React フック。第2引数「依存配列」を指定すると、依存配列に変更があった場合にのみ、第1引数で指定した関数が実行される。

基本形

react.jsx
useEffect(() => {
  /* 第1引数には実行させたい副作用関数を記述 */
}, [依存する変数の配列]) // 第2引数には副作用関数の実行タイミングを制御する依存データを記述

useContext

useContext は、既存の機能「Context」(a.k.a: データをアプリケーション内の深くまで渡す機能、pops のバケツリレーをしないで済む機能)をよりシンプルに分かりやすく書ける React フック。

基本形

react.jsx
const NumberContext = React.createContext()
const value = useContext(NumberContext)

useReducer

useReducer は、状態管理のための React フックで、useState と似たような機能がある。

useStateuseReducer に内部実装されており、 useReducer は複数の state を管理するのに適している。

基本形

react.jsx
const [state, dispatch] = useReducer(reducer, '初期値')

useCallback (+ React.memo)

useCallback は、メモ化したコールバック関数を返す React フックで、パフォーマンス向上に利用される。

React.js では親コンポーネントが再描画されると子コンポーネントも一緒に再描画される。再描画が繰り返されると、描画の数だけパフォーマンスなどに影響がある。
そこで React.memo を利用して、再描画の前に等価性のチェックを行い、必要な場合にのみ再描画をする手法がある。
それと併用されるフック関数が useCallback である。

useCallback でメモ化されたコールバック関数は、React.memo でメモ化されたコンポーネントへ渡すことで不要な再描画をスキップ出来るようになる。

基本形

react.jsx
const callback = useCallback(関数, [deps])

useMemo

useMemo は、関数の結果を保持するための React フックで、何度行っても結果が同じ場合の値などを保存(メモ化)し、そこから値を再取得する。

useMemo の第2引数「依存配列」の値に変化がない場合は、キャッシュから値を取得する。

基本形

react.jsx
const sampleMemoFunc = () => {
  const memoResult = useMemo(() => hogeMemoFunc(), [])

  return <div>{memoResult}</div>
}

useRef

useRef は、関数コンポーネントの参照を行う React フック。ちなみに、クラスコンポーネント参照の場合は ref 属性を利用して要素を参照する。

useState の場合は state が変更される度にコンポーネントの再描画が発生するが、useRef は値が変更になっても、コンポーネントの再描画は発生しない特徴がある。

基本形

react.jsx
const refObject = useRef(initialValue)

const number = useRef(100)
console.log(number.current) // 100
maechanmaechan

GraphQL API をもっと理解する

GraphQL API をさらに手に馴染ませるために GitHub が公開している「GraphiQL」でいろいろ試す。

今回の教材

https://www.udemy.com/course/graphql-with-react/

Memorandum

  • REST API はマクドナルド方式(材料やその量が決まっている、変更できない、仕様がガチガチ)、GraphQL API はサブウェイ方式(個人の好みに応じて材料の量などを変更することができる)
  • GraphQL API の「Request Method」は必ず POST になる
  • Prettify は query 文字や不要なコードなどを削除する
    • ※ Operation Name はその限りではない
  • Operation Name は「Request Payload」に付与される
  • データの取得は「Query」を、データの更新・削除は「Mutation」を利用する

Aliases

同じフィールドに対して処理を行いたい場合、API で定義されているフィールドを複数使用すると構文エラーが発生する。その場合はフィールド名に別名(Aliases:エイリアス)を付与することで回避できる。

 user1: user(login: "maecha") {
    bio
    login
    avatarUrl
  }
  user2: user(login: "gipcompany") {
    bio
    login
    avatarUrl
  }

Fragments

上の Aliases の記述は重複したコードがあり冗長である。Fragments を利用するともっといい感じに書くことができる。フィールドへは展開構文で適用する。

{
  user1: user(login: "maecha") {
    ...commonFields
  }
  user2: user(login: "gipcompany") {
    ...commonFields
  }
}

fragment commonFields on User {
  bio
  login
  avatarUrl
}

Operation Name

query を識別するための機能。

query getUser1 {
  user(login: "maecha") {
    bio
  }
}

query getUser2 {
  user(login: "gipcompany") {
    bio
  }
}

Variables

動的な引数(変数)の使い方。query () に対応する引数を指定する。

query ($login: String!) {
  user(login: $login) {
    bio
    name
  }
}

// Query Variables
{
  "login": "maecha"
}

Mutation

データの更新・削除は Mutation を利用する。

query repository {
  repository(owner: "maecha", name: "react-hooks-examples") {
    id
    name
    url
  }
}

mutation addStar {
  addStar(input: {
    starrableId: "xxxxxxxxxxx"
  }) {
    starrable {
      id
      viewerHasStarred
    }
  }
}

mutation removeStar {
  removeStar(input: {
    starrableId: "xxxxxxxxxxx"
  }) {
    starrable {
      id
      viewerHasStarred
    }
  }
}

Inline Fragments + __typename

API から返却されるデータの型を動的に指定する。

query searchWithTypeName {
  search(query: "We work hard", type: USER, first: 2) {
    nodes {
      __typename
      ... on User {
        id
        name
        url
      }
      ... on Organization {
        id
        name
        url
        projectsUrl
      }
    }
  }
}

Type System, Scalar Types, Object Types, Schemas

データの型について。API 作成時に意識する部分。

Scalar Types の確認

http://spec.graphql.org/June2018/#sec-Scalars

Object Types の確認

http://spec.graphql.org/June2018/#sec-Objects

Pagination (The Relay-Style Cursor Pagination)

The Relay-Style Cursor Pagination は、GraphQL を用いたページネーションでメジャーな実装スタイルの一つ。GitHub ではユーザーが検索した際のページネーションに利用されている。

図で確認


via: https://qiita.com/gipcompany/items/ffee8cf0b1522a741e12


via: https://qiita.com/gipcompany/items/ffee8cf0b1522a741e12

登場人物

  • Connection:ある塊単位でデータを要求するその「ある塊」のこと
  • Page:Edges と pageInfo の2つで構成された Connection が示す情報の集まり
    • Edge:位置情報を示す Cursor と情報そのモノである Node(データを抽象化した呼び名)で構成されている
    • pageinfo:startCursor、endCursor、hasPreviousPage、hasNextPage で構成されている
      • startCursor:先頭の Edge がもつ Cursor(位置)情報
      • endCursor:最後尾の edge がもつ Cursor(位置)情報
      • hasPreviousPage:前のページが存在するかどうか
      • hasNextPage:次のページが存在するかどうか

GraphiQL でレポジトリ検索

query searchRepository(
  $first: Int,
  $last: Int,
  $after: String, 
  $before: String,
  $query: String!
) {
  search(
    first: $first,
    last: $last,
    after: $after,
    before: $before,
    query: $query,
    type: REPOSITORY
  ) {
    repositoryCount
    pageInfo {
      startCursor
      endCursor
      hasNextPage
      hasPreviousPage
    }
    edges {
      cursor
      node {
        ... on Repository {
          id
          name
          url
        }
      }
    }
  }
}

// Query Variables
{
  "after": null,
  "before": null,
  "first": 5,
  "last": null,
  "query": "フロントエンドエンジニア"
}

参考記事

https://qiita.com/gipcompany/items/ffee8cf0b1522a741e12

作成したアプリ

今回の教材で作成したものは「GitHub レポジトリ検索アプリ」で、サーバーとのデータのやり取りは GraphQL を介して行っている。

https://github.com/maecha/udemy-graphql-with-react-intro-search-repos

アプリの機能はレポジトリの検索、スターの追加・削除、ページネーションの 3 つ。アプリの内部では Apollo Client の「writeQuery」や React Refs を利用している。

maechanmaechan

Next.js の素振りをもう一度

これまで生の React.js を触る機会がほとんどで Next.js はあまり触れてこなかったので、もう一度このフレームワーク界隈を調査したりしていく。

maechanmaechan

手始めの読みもの

基礎的なことのインプットは、読みやすそうなサードパーティ記事と聖書で。

https://kinsta.com/jp/blog/nextjs-vs-react/

https://nextjs.org/docs

聖書に関しては、有志の方々が運営している日本語翻訳版もある。有り難やあ🙏

https://nextjs-ja-translation-docs.vercel.app/

まずはこのあたりを読んで「完全に理解した」と言いたい。

翻訳版の内容で理解しておくと良いことありそうな項目

  • プリレンダリング
  • データ取得
  • 画像最適化
  • ルーティング
  • API ルート
  • ミドルウェア
  • React 18

翻訳は完全にされていないので、原本も確認しながら読んでいく。

maechanmaechan

簡単なアプリを写経しながら手に馴染ませていく

Next.js(というか最近は React.js からも少しの間離れて、生の JavaScript を触っていた経緯もある)の全体像を俯瞰したい気持ちがあったので、簡単ばウェブアプリを写経して、世界観を感じたり、コードを手に馴染ませたりしていく作業を行った。

作成したアプリ

Zenn で人気のある教材「Next.js 14 アプリ開発のあらすじ」を使って作ったアプリをデプロイまでやってみた。

デモ

https://demo-image-post-service.vercel.app/

GitHub リポジトリ

https://github.com/maecha/demo-image-post-service/tree/main

このアプリの主な技術スタックはこちら。

  • Vercel
    • ホスティング、DB(PostgreSQL)として利用
  • Cloudflare R2
    • 画像サーバーとして利用
  • Clerk
    • ユーザーの認証サービスとして利用
  • shadcn-ui/ui
    • コンポーネントライブラリとして利用
  • Tailwind CSS
    • CSS フレームワークとして利用
  • Prisma
    • ORM として利用

shadcn-ui/ui は初めて使うスタックで、利用するコンポーネントは npm でパッケージ化せず、CLI 経由でインストールしていく方式を採用している。コンポーネント自体は低レイヤーなもので構成されている。

各ページは pages/ ディレクトリを用意せず、app/ ディレクトリに直接パスと同じ名前のディレクトリを作成して、そこにファイルを置いていくスタイルを取っていた。これは App Router を採用しているからかなあ。

The App Router works in a new directory named app. The app directory works alongside the pages directory to allow for incremental adoption. This allows you to opt some routes of your application into the new behavior while keeping other routes in the pages directory for previous behavior. If your application uses the pages directory, please also see the Pages Router documentation.

https://nextjs.org/docs/app/building-your-application/routing#the-app-router

インストールされたコンポーネントは components/ui に配置される(配置場所は components.json をいじったら変更できそう)。

インストールされたコンポーネントはこんな感じで呼べる。

import { Dialog } from "@/components/ui/dialog";

データ取得(サーバーとの通信とか)は /app/actions/ にまとめている。その中の lib.ts で Prisma のラッパーを定義。

middleware.ts で認証まわりのステータスの確認やリダイレクトの判定等をしている。