kjugk’s diary

web フロントエンド技術を中心に、試したこと、学んだことを紹介していきます。

個人開発サービスをリリースしました!

久しぶりに個人開発サービスをリリースしたので、その紹介と使った技術を簡単に振り返ってみようと思います。

作ったもの

flashcard という、問題集作成・学習サービスです。 www.flashcard.kjugk.com

問題集に複数の問題と答えのペアを登録できて、カードをめくりながら答え合わせをすることができます。

ピンと来る方もいると思いますが、Quizletという有名サービス のクローン に多大な影響を受けています🤓

モチベーション

以下の技術的な興味を満たすために、作成を開始しました。

  • 単純に最新の React 触ってみたい。
  • Redux を使わない状態管理(hooks) でどこまでできるか試してみたい。
  • serverless アーキテクチャーで API を作成してみたい。

デザインは友人のデザイナーにモック作成を発注し、Figma で細部を詰めていきました。 作成期間は、週末を使って大体 3ヶ月くらいかかっています。

使ってる技術

サーバーサイド

API

API にはなるべくコストをかけたくなかったので、serverless フレームワークを使い、AWS の基本的なサーバーレススタック(API Gateway + Lambda + DaynamoDB)で REST API を構成しました。最初は DynamoDB がなかなかとっつきづらくて苦戦しましたが、Global Secondary Indexes の概念を理解してからは割とすんなり実装出来ました。(とはいえ複雑なスキーマを扱う実戦レベルには程遠いのでまだまだ勉強が必要・・🧐)

今回はエンドポイントごとに handler を作成しましたが、handeler をひとつにして express でルーティングすることも可能なようなので、次は express でやってみたい。

github.com

認証

API Gateway との統合が簡単なことから、Cognito を採用しました。 Javascript SDK は Apmlify に統合されたようで、ドキュメントも充実しており、React との統合も特に問題は無かったです。

docs.amplify.aws

フロントエンド

フレームワーク

Create React App(CRA) で作成しました。 ホットリロードや env の仕組みや TypeScript の環境が一瞬で作れるだけでも、React のSPA 開発で採用しない理由は無いかと。

create-react-app.dev

コンポーネント

material-icons 以外は UIフレームワークを使わずに自作しました。 FunctionComponent で状態を持つコンポーネントガリガリ書いていけるのが最高😀 自分は細かいインタラクションの実装が弱いことが判明したので、学んでいきたいです。

状態管理

状態管理は、redux を使わずに hooks(useState, useReducer + global context) だけで行っています。 これくらいの規模なら hooks で十分でしたが、redux-thunk のような middleware で副作用のある処理を扱いたかったり、store が複雑で細かいデバッグが必要な場合は、これからも redux を使うと思います。

form

フォームは react-hooks-form を採用しました。formik や redux-form と比べると記述量をかなり削減できるので、とても良いライブラリだと思います。カードの増減なども、ライブラリが配列操作のAPIを提供してくれているのでとても簡単に実装出来ました。

react-hook-form.com

css

CSS は styled-components で定義しています。props で切り替わるスタイルの定義は、やっぱり classNames で頑張るより断然楽でした。css のプロパティに対する値をエディターが列挙してくれると良いなぁ、と思うのでやり方探してみよう。あと、コンポーネント内の CSS をなるべく減らしたいので、tailwindcss と styled-components 共存が出来ないか調べてみる。

styled-components.com

tailwindcss.com

test

テストは、今の所ユニットテストのみで、util 系は Jest, React コンポーネントは React testing library で書いています。後からテストを書いていくと、コンポーネントの分割をやり直したくなることが多いので、やっぱり最初からテスト回しながら書いておけばよかった、と反省。とはいえ testing library をひととおり触れたのは良かった。

testing-library.com

フロントエンドのデプロイ

アプリケーションは Vercel でホストしています。

最初は練習ために、 CloudFront と S3 でホストし、 Code Pipeline で CD の仕組みを自作(staging ブランチにマージしたらステージング環境にデプロイ)していたのですが、Vercel があまりにも便利なので途中で置き換えました。CRA を使っている場合は追加の設定無しでそのまま統合できるのも強い。

Vercel で特に気に入っているのは、Github 連携するとリモートブランチを更新するたびに、最新のコードでアプリケーションがデプロイされる仕様です。これだけで開発スピードがかなりアップするので、これからもホスティング環境はまずは Vercel を第一候補として検討していきたいです。(Netlify とかでも同様のことはできると思うので調べてみよう。)

今後の実装予定機能

以下のような機能を考えているので、これからもコツコツ機能を追加していこうと思っています。

  • 問題集を公開できるようにする。
  • 答えに画像・注釈を追加できるようにする。
  • 問題集にタグ付けできるようにする。
  • 問題集を検索できるようにする。
  • 採点できるようにする。

感想

やっぱり個人開発楽しい!というのが素直な感想です。失敗を気にせず serverless や React 、Vercel で色々試してみることが出来たのは、自分の知的好奇心を満たしつつ技術力アップにもつながったかなと思います。

あとモチベーションを維持するには、よっぽど実現したいアイディアが無い限り、手に馴染んでるものばかりではなくて、ちょっと興味あるけど触ったことない技術を選んでいくのがいいのかなー、と思ったので、次は Next.js と headless CMS を使って全ページ SSG するようなサービス作ってみようと思っています。(無難に個人サイトかな?)

SaaS をうまく組み合わせれば低コストでどんどんアイディアを実現できるので、フロントエンドエンジニアにとっては良い時代になったなー、としみじみ感じています 😀

Set-Cookie ヘッダーの SameSite 属性について

ちょっと前の話になりますが、Chrome で Set-Cookie の SameSite 属性のデフォルトの挙動が変更になりました。

www.chromium.org

この変更内容を正確に理解できてなかったので、改めてSameSite 属性の仕様から対応方法まで整理してみます。

サードパーティークッキー

本題に入る前に、まずはサードパーティークッキー についておさらいしておきます。

ブラウザは HTTPリクエストを送信する際、リクエストするサイトが発行した Cookieを、毎回リクエストヘッダーに載せます。

これは今自分がどのサイトにいるかは関係なく、例えばユーザーがサイトAにいる場合、サイトBに対するリクエストにも、サイトBのCookieがブラウザに保管されていれば、Cookie を送信します。 この場合の、サイトB に送信しているCookie を、サードパーティークッキーと呼びます。

簡単な例として、example.com へ下記のような index.html をリクエストした場合を考えてみます。

GET https://example.com/index.html

<html>
   // 省略
   <img src="https://example2.com/img.png" />
   // 省略
</html>

画像は https://example2.com/img.png へリクエストされるので、もし example2.com がレスポンスにクッキーを設定している場合は、このクッキーはサードパーティークッキーとして、次回以降の example.com へのリクエストでも、 example.2.com へ送信されます。

GET https://example2.com/img.png

200 OK
Set-Cookie: value=1; <= サードパーティークッキー

このオープンな仕様は様々なユースケースで利用しやすい反面、ユーザーが意図していない行動履歴トラッキングに利用されたり、この仕組を利用したCSRF攻撃の手法が存在するなど、プライバシー保護やセキュリティ観点での懸念があります。

www.ipa.go.jp

SameSite 属性

SameSite 属性を導入することで、Cookie を送信する範囲を制限することを宣言できます。SameSite 属性は以下の3つの値を取ります。

Strict

クッキーは「発行されたのと同じサイトに対してのみ」送信される。

最も強い制約だが、例えばセッションID などの 認証用Cookie に Strict をセットすると、他サイトから訪れたユーザーはセッションが有効な場合でも必ずログインし直さなくてはならなくなるなど、想定している Cookieの利用も制限してしまう可能性がある。

Lax

基本はStrict と同じだが、「トップレベルのナビゲーション」では Cookieを送信できるようにする。

トップレベルのナビゲーションというのは、ほかサイトからのリンクによるナビゲーションのことなので、例えば、Strict では制限されていた、他サイトからのリンクをたどって訪問してきたユーザーのセッションを維持することができる。

トップレベル以外、つまり iframe や XHR でのほかサイトへのリクエストでは、Cookie は送信されない ことに注意。

None

サイト間リクエストを制限しない。その代わり、Secure 属性の指定が必要。(Secure 属性が無いとCookie はブラウザに保存されない) Set-Cookie: value=1;SameSite=Strict; Secure

Chrome80のアップデート

Chrome80 のアップデートから、SameSite 属性の指定されていない CookieLax が指定されているものとして扱うようになりました。

www.chromium.org

この変更により、サードパーティークッキーとしての利用を想定しているCookie は、SameSite:None; Secure を指定しないと、従来想定していた形では利用できなくなるので、対応が必要になります。

なおこの挙動は、2020年10月現在、Chrome 以外でも多くのブラウザでデフォルトの挙動として実装されています。

developer.mozilla.org

まとめ

以上を踏まえて、 SameSite 属性の指定方法をまとめると、

  • クッキーをサードパーティークッキーとして明示するには、 SameSite=None; Secure を指定する。
  • 同一サイト内からしか受信しないクッキーは、SameSite=Strict を指定する。
  • その他はデフォルトで Lax となるが、デフォルトの挙動が違うブラウザ考慮して、明示的に SameSite=Lax を指定するのが望ましい。

となります。

サードパーティークッキーの今後

Googleサードパーティークッキーの段階的廃止を検討し、新しいエコシステム(プライバシーサンドボックス)への移行を検討しています。

gigazine.net

広く利用されている仕様が急に置きかわることはないでしょうが、サードパーティークッキーの仕組みに依存している場合はこちらの動向もチェックしておく必要がありそうです。

参考URL

Cache-Control ヘッダーの state-while-revalidate ディレクティブについて

Next.js のレスポンスヘッダーを眺めていたら、Cache-Control ヘッダーに stale-while-revalidate という見慣れないディレクティブが指定されたいたので、気になって調べてみました。

検証(条件付きリクエスト)

stale-while-revalidate の話に入る前に、HTTPでリソースの更新を検証する仕組みを軽く振り返っておきます。

Etag

リソースの更新を検証する方法のひとつとして定義されている Etag(Entity Tag) は、リソースの特定バージョンの識別子です。 ETag 値が生成される方法は仕様には明記されていないので、コンテンツのハッシュ、最終更新タイムスタンプのハッシュなどが使われます。

RFC

RFC 7232 - Hypertext Transfer Protocol (HTTP/1.1): Conditional Requests

レスポンス例

Cache-Control: public, max-age=10, must-revalidate
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"

Etag を受け取ったクライアントは、そのリソースのキャッシュが有効でなくなった場合、If-None-Match ヘッダーをリクエストに含めることで、サーバーにリソースの検証を要求できます。

リクエス

If-None-Match "33a64df551425fcc55e4d42a148795d9f25f89d4"

サーバーは、クライアントから送信された ETag を現在のバージョンと比較し、値が一致する場合は 304 Not Modified を「body 無し」で返します。 一致しない場合は新しいレスポンスを返して、クライアントはキャッシュを更新します。

f:id:kjugk:20200928221831p:plain
etag

更新されていないリソースのレスポンスは body が空になるので、通信量とサーバーのリソースを大幅に削減することができます。 ただし、ここで注目したいのは、「リソースが更新されていない場合でも、検証時の クライアント <=> サーバー間のリクエスト/レスポンスは発生している」ということです。

stale-while-revalidate

より効率の良いキャッシュの検証/更新方法として、Cache-Control の拡張仕様の中で、stale-while-revalidate が定義されています。

RFC

RFC 5861 - HTTP Cache-Control Extensions for Stale Content

2020年9月現在、ほとんどのモダンブラウザ、CDNで利用可能です。 Cache-Control - HTTP | MDN

stale-while-revalidate を有効にするには、下記のように Cache-Control のディレクティブとして秒数を指定します。

Cache-Control: max-age=3300, stale-while-revalidate=300

この例だと、max-age で指定されている 3300秒が過ぎたらリソースを検証する必要がありますが、次の300秒までは引き続き古く(staleに)なったキャッシュを利用することができます。 クライアントは、stale になったキャッシュを最初に利用する際に、「非同期に」サーバーに検証リクエストを行い、リソースが古い場合は、キャッシュを更新しておきます。

f:id:kjugk:20200928215423p:plain

この仕組みによって、クライアントではできるだけキャッシュを使いつつ、キャッシュの検証/更新にかかる待ち時間を最小限に抑えることが可能になります。

サンプル

実際に動かせるサンプルとして、下記の記事がとても参考になったので紹介します。 Keeping things fresh with stale-while-revalidate

このサンプルでは、レスポンスに cache-control: max-age=1, stale-while-revalidate=59 が指定されているので、リクエストから1~60秒以内のリクエストでは、古いキャッシュを使いつつ非同期に再検証が走っています。

Chrome で何回かリクエストを送っていると、レスポンスの値が更新されているのに、リソースは disk cache から配信されていることが確認できると思います。

Next.js のサーバーサイドレンダリングについて

前回の記事で、Next.js の StaticGeneration について紹介しましたが、今回は Server-side Generation の仕組みを紹介します。

バージョン

Next.js 9.5.2

getServerSideProps

page コンポーネントgetServerSideProps をエクスポートすると、そのページは、「リクエストの度に」ページをレンダリングして返すようになります。

// /pages/posts/index.tsx
import { GetServerSideProps, InferGetServerSidePropsType } from "next";

interface Post {
  id: string;
  title: string;
}

export const getServerSideProps: GetServerSideProps<{ posts: Post[] }> = async () => {
  const response = await fetch("http://some-kind-of-api/posts");
  const posts = (await response.json()) as Post[];

  return {
    props: {
      posts,
    },
  };
};

export default function Posts({
  posts,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
  return <ul>{posts.map((p) => <li>{p.title}</li>)}</ul>;
}

この状態で npm run build しても HTML は生成されませんし、Response Header を確認すると、Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate となっているので、CDN も含めてレスポンスはどこにもキャッシュされなくなります。

Server-side Rendering の使い所

Server-side Rendering をする場合、当然リクエスト毎にデータフェッチ => レンダリング処理が走るかつ、特別な設定をしない限り CDN にもキャッシュされないので、パフォーマンスは Static Generation よりでません。

パフォーマンスを犠牲にしてでも常に最新のデータを表示したいページ(例えばリアルタイムに変動する商品在庫を表示するECサイトの商品ページなど)のみ、Server-side Rendering の利用を検討することになるでしょう。ただその場合も、とりあえず StaticGeneration したページを返してから、クライアントサイドでリアルタイムデータをフェッチ => 反映させることも可能なので、Server-side Rendering するかどうかは、アプリケーションのパフォーマンス要件と合わせて慎重に選択する必要があります。

まとめ

前回と今回の記事で Next.js の事前ページレンダリングについて紹介してきました。Next.js でページを生成する方針をざっくりまとめると、以下のようになります。

  • Incremental Static Regeneration が導入れた Next.js 9.5以降では、public なページは、パフォーマンスを上げるために、できるだけ StaticGeneration する。
  • 常にリアルタイムなデータがほしい場合のみ、Server-side Rendering を使う。
  • 事前にレンダリングする必要が無いページ(ログイン必須のダッシュボードページなど)は、Static Generaton で AppShell的な静的ページを返してから、クライアントでレンダリングする方法も検討する。

そもそも Next.js が必要になるアプリケーションは、SEOが最重要課題となるはずなので、できるだけ静的ページを CDN から配信できるように構成・設計するのが良いと思います。

Next.js の事前レンダリング(Static Generation)について整理する

Next.js がページをレンダリングする方法として、Static GenerationServer-side Rendering があります。

  • Static Generation
    • HTMLはビルド時に生成され、リクエストごとに再利用される。
  • Server-side Rendering
    • HTMLはリクエストごとに生成される。

この記事では、Next.js の Static Generation の概要を公式ドキュメント を参考に紹介します。

バージョン

Next.js 9.5.2

サンプルアプリケーション

https://nextjs-ssg-demo.vercel.app/posts

外部データに依存しないページ

外部データに依存しないページは、「ビルド時」に html、及び関連する静的ファイル(js, css など)が生成されます。ビルドされた html ファイルは、/.next/server/pages ディレクトリに配置されます。

例しに、/pages/index.jsxnpm run build でビルドすると、/.next/server/pages/index.html ページが生成されました。

// /pages/index.tsx
export default function Home() {
  return <h1>hello</h1>;
}

npm start で Next.js のサーバーを起動して http://localhost:3000/ にアクセスすると、このファイルが返されるのが確認できます。コンテンツがロードされると、最小限の JavaScript がクライアントで実行(ハイドレーション)されます。

外部データに依存するページ

getStaticProps

外部データ(API,ファイルなど)に依存するページの場合は、ページコンポーネントgetStaticProps をエクスポートすることで、ビルド時にデータをフェッチしつつ、ページを生成できます。

下記の例では、getStaticPropsAPI から記事一覧を取得し、ページコンポーネントの props に渡して、記事一覧ページを生成しています。(なお、getStaticProps は、クライアントサイドのコードにはバンドルされないので、API の SecretKey などの秘匿情報を含めてもOK。)

// /pages/posts/index.tsx
import { GetStaticProps, InferGetStaticPropsType } from "next";

interface Post {
  id: string;
  title: string;
}

export const getStaticProps: GetStaticProps<{ posts: Post[] }> = async () => {
  const response = await fetch("http://some-kind-of-api/posts");
  const posts = (await response.json()) as Post[];

  return {
    props: {
      posts,
    },
  };
};

export default function Posts({
  posts,
}: InferGetStaticPropsType<typeof getStaticProps>) {
  return <ul>{posts.map((p) => <li>{p.title}</li>)}</ul>;
}

getStaticPaths

Next.js の Dynamic Routes を使うページの場合は、getStaticPaths で事前に取得したパラメーターを、getStaticProps に渡すことで、Static Generation することができます。

下記は、/post/[id] というパスでアクセスできる記事詳細ページの例です。 getStaticPaths で記事の id 一覧を取得して返しています。ビルド時は、この配列の要素分だけページが生成されます(例:.next/server/pages/post/1.html)

// /pages/post/[id].tsx
import { GetStaticProps, InferGetStaticPropsType, GetStaticPaths } from "next";

interface Post {
  id: string;
  title: string;
}

export const getStaticPaths: GetStaticPaths = async () => {
  const response = await fetch(`http://some-kind-of-api/posts`);
  const posts = (await response.json()) as Post[];

  return {
    paths: posts.map(p => ({params: {id: p.id}})), // この配列の要素分ページが生成される。
    fallback: false,
  };
};

export const getStaticProps: GetStaticProps<{ post: Post }> = async ({
  params,
}) => {
  const response = await fetch(`http://some-kind-of-api/post/${params.id}`);
  const post = (await response.json()) as Post;

  return {
    props: {
      post,
    },
  };
};

export default function Post({
  post,
}: InferGetStaticPropsType<typeof getStaticProps>) {
  return <h1>{post.title}</h1>;
}

Incremental Static Regeneration

ここでふと、「ビルド時にフェッチしたデータでページを生成するということは、ビルド後に更新・作成されたデータで事前レンダリングするには、毎回アプリケーション再ビルドが必要になるのだろうか?」という疑問が湧いてきたのですが、Next.js 9.5 で導入された Incremental Static Regeneration の仕組みを使えば、再ビルドなしにこの問題を解消できます。

データの更新に対応する

ビルド後に更新されたデータでページを再レンダリングするには、 getStaticProps の戻り値で、revalidate オプションを指定します。

下記の例だと、記事ページリクエスト時に、前回キャッシュされた時間から10分以上経過していれば、ページの更新を確認し、更新があればページを再生成します。 データの更新頻度に合わせてこの値を設定すれば、StagicGeneration の恩恵を受けつつ、フレッシュなデータを表示させることができます。

// /pages/posts/index.tsx
import { GetStaticProps, InferGetStaticPropsType } from "next";

interface Post {
  id: string;
  title: string;
}

export const getStaticProps: GetStaticProps<{ posts: Post[] }> = async () => {
  const response = await fetch("http://some-kind-of-api/posts");
  const posts = (await response.json()) as Post[];

  return {
    props: {
      posts,
    },
    revalidate: 600, // ページが再生成されるまでの時間を秒で指定する
  };
};

export default function Posts({
  posts,
}: InferGetStaticPropsType<typeof getStaticProps>) {
  return <ul>{posts.map((p) => <li>{p.title}</li>)}</ul>;
}

データの追加に対応する

getStaticPaths を使うページの場合、ビルド後に作成された記事をリクエストすると、当然ページが存在しないのでりクエストは失敗します(404)。この状態を回避するには、getStaticPaths の戻り値に fallback: true を指定します。

これにより、ファイルが存在しないパスがリクエストされた場合は、初回のみフォールバックページを返し、次回リクエスト以降は生成済みファイルを配信することができます。

下記の例では、ビルド時には getStaticPaths で渡したパスのページの他に .next/pages/post/[id].html という名前で、フォールバックコンテンツを含んだファイルが生成されます。

存在しないページがリクエストされた場合、まずブラウザにはフォールバックページが返され、ページの生成が完了したら動的にページの内容が置き換えられます。次回以降のリクエストでは生成済みページが返されます。(まだこの挙動がどうやって実現されているのか理解できてない・・ ソース読んでみよう。)

// /pages/post/[id].tsx
import { GetStaticProps, InferGetStaticPropsType, GetStaticPaths } from "next";
import { useRouter } from "next/router";

interface Post {
  id: string;
  title: string;
}

export const getStaticPaths: GetStaticPaths = async () => {
  const response = await fetch(`http://some-kind-of-api/posts`);
  const posts = (await response.json()) as Post[];

  return {
    paths: posts.map(p => ({params: {id: p.id}})),
    fallback: true, // リクエストされたパスが存在しない場合は、ページを生成する。
  };
};

export const getStaticProps: GetStaticProps<{ post: Post }> = async ({
  params,
}) => {
  const response = await fetch(`http://some-kind-of-api/post/${params.id}`);
  const post = (await response.json()) as Post;

  return {
    props: {
      post,
    },
  };
};

export default function Post({
  post,
}: InferGetStaticPropsType<typeof getStaticProps>) {
  if (router.isFallback) return <div>Loading..</div> // 初回リクエストの場合は、router.isFallback が true になるので、フォールバックコンテンツを返す。
  if (post === undefined) return <div>お探しのページは見つかりませんでした。</div> // post が undefined === コンテンツが削除された。
  
  return <h1>{post.title}</h1>;
}

まとめ

  • Next.js では、基本的に全てのページを Static Generation する。
  • ページコンポーネントで getStaticPath をエクスポートすることで、外部データを使って Static Generation できる。
  • ページコンポーネントで getStaticPaths をエクスポートすることで、DynamicRouting のページも Static Generation できる。
  • Incremental Static Regeneration の仕組みを使うことで、ビルド後に作成・更新されるページでも Static Generationできる。

以上から、Next.js では非常にシンプルな仕組みでページを Static Generation できることがわかりました。 基本的には、public なページはすべて Static Rendering できるようにアプリケーションを設計していくのが良いと思います。

特に Vercel でアプリケーションをホスティングする場合は、細かいCDN の設定をしなくてもエッジロケーションから事前レンダリングしたファイルを配信できるので、アプリケーションの速度、開発リソースの面で大きな恩恵を受けられると思います。

次の記事では、Server-side Rendering についてまとめてみます。

nvm と avn-nvm で自動的に Node.js のバージョンを切り替える

Node.js は半年ごとにバージョンアップが行われ(Releases | Node.js)、その度にv8のバージョンアップや、実験的な仕様がどんどん実装されています。 そのため、本番・開発環境で Node.js のバージョンを統一させることは重要です。

この記事では、Docker を使わずにローカルマシンで複数のバージョンの Node.js を管理する方法と、プロジェクト毎に自動的に Node.js のバージョンを切り替える方法を解説します。

なお、Node.js のバージョン管理ツールとしては、nnodebrew がありますが、この記事では現時点で最も github のスター数が多い nvm を使います。

環境

OS: macOS 10.15.5

nvm で複数バージョンの Node.js を管理する

インストール

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash

インストール可能な Node.js のバージョンを表示

nvm ls-remote

特定のバージョンの Node.js をインストール

nvm install 14.5.0

インストール済みの Node.js のバージョンを表示

nvm list

手動で Node.js のバージョンを切り替える

nvm use 12.8.0

デフォルトバージョンを変更

nvm alias default 14.5.0

以上で、複数のバージョンの Node.js を、簡単に管理・切り替えることができます。一人で単一のプロジェクトを開発しているならこれで十分かもしれませんが、複数のプロジェクトを複数人で開発している場合、手動で毎回 Node.js のバージョンを切り替えるのは非常に面倒です。うっかり古いバージョンの Node.js を使っていて、「ESModules の警告が出るんですが・・」みたいな状況が発生しかねないので、プロジェクト毎に自動で切り替えられるようにします。

プロジェクトごとにバージョンを切り替える

avnavn-nvm を使って、自動的に Node.js のバージョンを切り替えられるようにします。

インストール

npm install -g avn avn-nvm

セットアップ

avn setup

プロジェクトの Node.js バージョン指定

プロジェクトのルートディレクトリに .node-version を配置し、バージョンを semver 形式で指定します。

echo 14.5.0 > .node-version

以上で、.node-version ファイルがあるディレクトリに移動する度に、自動的に Node.js のバージョンを切り替えることができます。もし .node-version で指定した Node.js がインストールされていない場合、avn could not activate node 14.5.0 と警告がでるので、nvm install でインストールすればOKです。

あとは本番環境のバージョンと齟齬が出ないように、ビルド環境やホスティング環境で、バージョンをあわせておきます。 たとえば vercel なら、package.json の engines でバージョンを指定します。 Official Vercel Runtimes - Vercel

Render Props を使ってコンポーネントを再利用しやすくする

Render Props とは?

Render Props とは、複数のコンポーネントにまたがる横断的な関心事を一つのコンポーネントにまとめて、コンポーネントを再利用性しやすくするテクニックです。

この記事では、Render Props の使い方を、簡単なサンプルを作りながら確認してみます。

なお、今回作成したサンプルコードは CodePenで実行できます

See the Pen React Render Props example by Koji (@kjugk) on CodePen.

ビューポートへの表示を管理する例

例として、Intersection Observer API を使って、特定のコンポーネントがビューポートに入ったかを管理する例を考えてみます。

まずは、「コンポーネントがビューポートに入ったかを監視する処理」と、「コンポーネントがビューポートへ一度でも入ったことがあるかの状態」をカプセル化する、InViewport というコンポーネントを、下記のように作成しました。

React Render Props Example.

constructor で、inViewport という state を定義しています。これは、「コンポーネントがビューポートへ一度でも入ったことがあるかの状態」を表す state で、 IntersectionObserver に渡したコールバックにより、コンポーネントがビューポートに入ったら、true に更新されます。

次に render メソッドを見てみると、ここでは具体的なコンポーネントは描画していません。 props として渡された render メソッドに、inViewport state を渡して実行しているだけです。

それでは、このコンポーネントを利用する例を見てみます。

Use render props

このように、具体的なコンポーネントを描画する関数を、利用側で動的に定義できます。 こうすることで、InViewport コンポーネントで管理している状態とふるまいを、複数のコンポーネントから簡単に利用できるようになりました。

プロパティ名について

RenderProps プロパティ名は render である必要は無いので、props.children を使って下記のように書くこともできます。

children as Render Props.

まとめ

  • Render Props を使うと、状態や振る舞いをカプセル化して再利用できる。

参考リンク