Share
Embed
Transcript
66 slides
-
Profile // profile.jsonc { "name": " ", "alias": ["akfm_sato", "
"specialty": " ", "assignment": " ", "tags": ["Next.js", "UX"], "sns": { "X": "akfm_sato", "Zenn": "akfm", }, } 佐藤 昭文 あっきー"], フロントエンド 住まい -
今日のテーマ : モダンフロントエンド開発 エンジニア市場における当たり前を知っておくことは、常に重要 今日の目標: フロントエンドにおけるモダンな技術の知見や感覚を養うこと ハードスキル TypeScript, Lint, Formatter,
Storybook Next.js ソフトスキル チームで助け合う技術 -
Agenda 実案件同様チームメンバー同士教え教わりながら進めましょう タイムテーブル 内容 10:15~10:45 はじめに 10:45~12:00 Storybook 12:00~13:00 休憩
13:00~15:00 Next.js 15:00~17:00 17:00~18:00 課題(ブログ開発) 振り返り -
プログラマーの誓い https://qiita.com/diskshima/items/588c423977f42626910d 害を及ぼすようなコードは書きません。 2. 自分が書くコードがもっとも良いものであることを誓います。振る舞いや構造が悪いコードをリリースしま せん。 3. リリースごとにコードのすべてが意図通りに動くことの、速く、確実で、再現可能な証明も併せて作りま す。 4.
他の人の進みを妨げないよう、コマ目に、小さいリリースを行います。 5. 常にコードを躊躇なく、常に改善していきます。間違ってもコードを悪くしません。 6. 自分や他の人の生産性を最も高く保つようにします。生産性を下げるようなことはしません。 7. 他の人が自分をカバーできるよう、また、自分が他の人をカバーできるように保ちます。 8. 規模と正確性において正直な見積りを出します。確信のない約束はしません。 9. 常に学び、自分の技術を磨き続けます。 1. -
モダンフロントエンド開発 と を中心にモダンフロントエンドの開発スタイルを学ぶ Storybook Next.js 題材 を使ったコンポーネント駆動開発 Tailwind CSS, shadcn/uiを使ったコンポーネントの構築
Next.jsを使ったアプリケーション開発 全体を通して利用する技術 Storybook TypeScript Lint Formatter 時間の都合で省略する技術 AIツールの活用 テスト(Vitest/Playwright) -
復習 : React ここからは事前課題をこなした前提で進めます が開発した、コンポーネント指向のフレームワーク JSXというJSの拡張記法でコンポーネントを宣言できる コンポーネントは必要に応じて再実行(再レンダリング)され、常に値が最新の状態に保たれる Meta function Accordion({
children }: { children: React.ReactNode }) { const [isOpen, setIsOpen] = useState(false); } return ( <div> <button onClick={() => setIsOpen(!isOpen)}>toggle</button> {isOpen && <div>{children}</div>} </div> ); -
コンポーネント指向とは コンポーネント: の構成部品 UI 公式ドキュメント - 隅から隅までコンポーネント ほとんどの React アプリでは隅から隅までコンポーネントが使われます。つまり、ボタンのような再利用可能なところでのみ使うので
はなく、サイドバーやリスト、最終的にはページ本体といった大きなパーツのためにも使うのです。コンポーネントは、1 回しか使わな いような UI コードやマークアップであっても、それらを整理するための有用な手段です。 -
Storybook コンポーネント単位で見た目や使い方を確認することができる // button.stories.tsx import type { Meta, StoryObj }
from "@storybook/react"; import { Button } from "./Button"; const meta: Meta<typeof Button> = { component: Button, }; export default meta; type Story = StoryObj<typeof Button>; export const Primary: Story = { args: { primary: true, label: "Button", }, }; -
課題 : Storybookの起動 研修の をセットアップして、 を起動してみましょう Workspace Storybook 研修リポジトリのREADMEに従ってセットアップ 2.
packages/ui のREADMEに従ってStorybookを起動 1. -
Tailwind CSS, shadcn/ui @repo/ui のコンポーネントは tailwind CSS と shadcn/ui を利用している
Tailwind CSS な フレームワーク 現在のCSS情勢はTailwind人気が非常に高い Utility 1st CSS shadcn/ui コンポーネントライブラリ 一強まで言わずとも非常に人気 -
課題: コンポーネントの追加 を追加し、 Checkbox のStoryを作成 Storyはどんなパターンがあるといいだろう🤔 2. Cardを追加し、 Card のStoryを作成
見やすいStoryにするには、どんな children がいいだろう🤔 3. その他好きにコンポーネントを導入+Storyを作成してみましょう 1. Checkbox -
App Router Next.jsの新しいアプリケーション構築の仕組み app ディレクトリ配下に配置する React Server Componentsをサポートしてる デフォルトでServer Components
Server -> Clientの2層構造 強力なCache その他諸々新機能(多くて混乱するので割愛) -
React Server Components React Server Components(RSC)はアーキテクチャ名 Server Component: サーバー側でのみレンダリングされるComponent Client
Component: 従来からあるクライアント・サーバーどちらでもレンダリング可能なComponent データとUIをまとめてComponentとして考える設計思想に基づく -
のモチベーション RSC 従来からあったReactの問題を解決したかった デフォルトでより良いパフォーマンスの達成 ハイドレーション処理・バンドルサイズを減らせる 低速なクライアント<->サーバーの通信を減らせる サーバー間通信の方が高速でセキュア バックエンドへのフルアクセス より簡単に、Componentが必要なデータを取得できるようになった 中間層(gSSP,
tRPC, GraphQL, API Routesなど)の実装や設計が不要に(詳細は後述) 参考: https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md#motivation -
と との違い RSC SSR 従来からあるSSR(Server Side Rendering)と混乱しやすいが、Server Componentsは全く異なる概念 SSR: Client
Componentsをサーバーサイドでレンダリングすること RSC: Reactの新たなアーキテクチャ名称 Client Components: 従来からあるComponent Server Components: サーバーサイドでのみレンダリングされるComponent -
Server Components サーバーでのみ実行されるComponent Component自体をasyncにすることが可能(= fetch を直接的に扱える) useState などの一部hooksは利用できない Client Components/Server
Componentsどちらも含めることができる export async function Products() { // await const res = await fetch("https://dummyjson.com/products"); const product = await res.json(); 今までできなかったが、直接 } できる return ( <div> <pre> <code>product: {JSON.stringify(product)}</code> </pre> <Counter /> {/* Counter: Client Component */} </div> ); -
Client Components 従来からあるComponent "use client" はサーバーから見た時のクライアントサイドの入り口 import されるモジュールは、全てClientモジュールとなる "use client"
import { useState } from "react"; import { Child } from "@/components/Child"; export function Counter() { const [count, setCount] = useState(0) // Client Component } のみで利用可能 return ( <div> <p>count: >{count}</p> <button onClick={() => setCount(prev => prev + 1)}>increment</button> <Child /> </div> ) -
Client Components 従来からあるComponent children (などのprops)を除き、Server Componentsを含むことはできない "use client"; import {
useState } from "react"; export function Accordion({ children, }: { children: React.ReactNode; // Server Components }) { const [isOpen, setIsOpen] = useState(false); } return ( <div> <button>toggle</button> <div>{children}</div> </div> ); も可! -
Server Action formの action からサーバー側の関数を実行できる "use server" はクライアントから見た時のサーバーへの入り口 // todo-create.ts
"use server"; async function createTodo(formData: FormData) { const title = formData.get("title"); // ...API POST DB ... } へ したり に保存するなど // page.tsx export default function Page() { return ( <form action={createTodo}> <input type="text" name="title" /> <button type="submit">Create</button> </form> ); } -
その他多くの機能 これらの詳細は で手を動かしながら short tutorial Nested Layout Error/Loading UI dynamic
route revalidate parallel route intercepting route PPR etc… -
課題: Helloにアクセスして、 world localhost:3000 のルートディレクトリより # Git $ cd apps/learn-next
$ pnpm dev のページが表示されることを確認しましょう Hello world -
課題: Nested layout を修正して共通のヘッダーを作成しましょう layout.tsx // app/layout.tsx export default function
RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="ja"> <body> <header>all page's header</header> {children} </body> </html> ); } -
課題 : New page ディレクトリを作成して新しいページを作成しましょう // app/page.tsx import Link from
"next/link"; export default function Page() { return ( <> <h1>Hello, Next.js!</h1> <Link href="/products">/products</Link> </> ); } -
課題 : New page ディレクトリを作成して新しいページを作成しましょう // app/products/page.tsx export default function
Page() { return <h1>Hello, Products page!</h1>; } -
課題 : New page ディレクトリを作成して新しいページを作成しましょう // app/products/layout.tsx export default function
RootLayout({ children, }: { children: React.ReactNode; }) { return ( <> {children} <footer>products page's footer</footer> </> ); } -
課題データを : Server Components + fetch して表示しましょう dummy fetch //
app/products/page.tsx import { Suspense } from "react"; export default function Page() { return ( <> <h1>Product Page</h1> <Suspense fallback={<div>Loading...</div>}> <Posts /> </Suspense> </> ); } async function Posts() { const res = await fetch("https://dummyjson.com/products"); const product = await res.json(); return ( <pre> <code>{JSON.stringify(product, null, 2)}</code> </pre> -
課題: でエラーが起きた時の Error UI は、 で定義しましょう page.tsx で強制的にエラーを起こすことで確認できます page.tsx UI
error.tsx // app/products/error.tsx "use client"; export default function Error({ error, reset, }: { error: Error & { digest?: string }; reset: () => void; }) { return ( <div> <h2>Error!</h2> <button onClick={() => reset()}>Try again</button> <pre>{error.message}</pre> </div> ); } -
課題: を使うために Client Components + useState を追加しましょう useState "use client"
// app/components/counter.tsx "use client"; import { useState } from "react"; export function Counter() { const [count, setCount] = useState(0); } return ( <div> <p>count: {count}</p> <button type="button" onClick={() => setCount(count + 1)}> increment </button> </div> ); -
課題: を使うために Client Components + useState を追加しましょう useState "use client"
// app/products/page.tsx import { Counter } from "app/components/counter"; import { Suspense } from "react"; export default function Page() { return ( <> <h1>Product Page</h1> <Counter /> <Suspense fallback={<div>Loading...</div>}> <Posts /> </Suspense> </> ); } // ... -
課題からサーバー側の関数を実行しましょう : Server Actions form // app/products/create-post.ts "use server"; export
async function createPost(data: FormData) { // skip validation const title = data.get("title"); console.log(`action called with title: "${title}"`); // ...API POST DB ... } へ したり に保存するなど -
課題からサーバー側の関数を実行しましょう : Server Actions form // app/products/page.tsx import { Counter
} from "app/components/counter"; import { Suspense } from "react"; import { createPost } from "./create-post"; export default function Page() { return ( <> <h1>Product Page</h1> <Counter /> <form action={createPost}> title: <input type="text" name="title" /> <button type="submit">create post</button> </form> <Suspense fallback={<div>Loading...</div>}> <Posts /> </Suspense> </> ); } // ... -
課題 : Dynamic route 動的な のページを作成しましょう URL // app/products/type.tsx export
type Product = { id: number; title: string; description: string; }; -
課題 : Dynamic route 動的な のページを作成しましょう URL // app/products/page.tsx import
Link from "next/link"; import type { Product } from "./type"; // ... async function Posts() { const res = await fetch("https://dummyjson.com/products"); const product: { products: Product[] } = await res.json(); return ( <> {product.products.map((product) => ( <div key={product.id}> <Link href={`/products/${product.id}`}>{product.title}</Link> </div> ))} </> ); } -
課題 : Dynamic route 動的な のページを作成しましょう URL // app/products/[id]/page.tsx import
{ Suspense } from "react"; import type { Product } from "../type"; export default async function Page({ params, }: { params: Promise<{ id: string }>; }) { const { id } = await params; } return ( <Suspense fallback={<div>Loading...</div>}> <ProductDescription id={id} /> </Suspense> ); // ... (次のページへ) -
課題 : Dynamic route 動的な のページを作成しましょう URL // app/products/[id]/page.tsx //
... (前のページより) async function ProductDescription({ id }: { id: string }) { const res = await fetch(`https://dummyjson.com/products/${id}`); const product: Product = await res.json(); } return ( <div> <h1>Product {product.title}</h1> <p>{product.description}</p> </div> ); -
課題 : use cache キャッシュしたいコンポーネントや関数には を追加しましょう "use cache" をファイルや関数の先頭に記述することで、キャッシュを有効化できる 有効期限は
cacheLife() で設定 import { cacheLife } from "next/cache"; async function Posts() { "use cache"; cacheLife("minutes"); } // ... "use cache" -
課題 : use cache キャッシュしたいコンポーネントや関数には を追加しましょう cacheTag('tag-name') でキャッシュにタグを付与でき、Server Functions内の updateTag('tagname')
でタグに紐づくキャッシュをrevalidateできる import { cacheTag } from "next/cache"; async function Posts() { "use cache"; cacheTag("products"); } // ... "use cache" -
課題 : use cache キャッシュしたいコンポーネントや関数には を追加しましょう cacheTag('tag-name') でキャッシュにタグを付与でき、Server Functions内の updateTag('tagname')
でタグに紐づくキャッシュをrevalidateできる "use server"; import { updateTag } from "next/cache"; export default async function revalidatePost() { // ... } updateTag("posts"); "use cache" -
課題 : ブログアプリケーション 今日学んだことを活かして、ブログアプリケーションを実装しましょう 前提 ディレクトリに作りかけのブログがあります APIの仕様は mock-server/openapi.json を参照してください 要件は非常に抽象的なので、周りや講師に相談しながらよしなに仕様を決めてください
要件 1. 一覧を10件ごとに表示するようにして下さい 2. 詳細ページを実装して下さい 3. 記事の新規作成機能を実装して下さい 4. 記事の更新ページを作ってください 5. 記事の削除機能を実装して下さい 6. formにvalidation機能を追加して下さい apps/blog-app -
今日のテーマ(再掲) : モダンフロントエンド開発 エンジニア市場における当たり前を知っておくことは、常に重要 今日の目標: フロントエンドにおけるモダンな技術の知見や感覚を養うこと ハードスキル TypeScript, Lint, Formatter,
Storybook Next.js ソフトスキル 教え教わる技術