経緯
拠所無き事情により新しいフレームワークを模索していたのですが、折角なので今更ながらメジャーストリームである Next.js に触っておこうかと思い立ちました。
手順
Node.js と Yarn は既に開発環境にあるので、サクッと yarn create next-app で Next.js のアプリを作ります。
> yarn create next-app
- create-next-app
√ What is your project named? ... SAMPLEAPP
√ Would you like to use TypeScript with this project? ... No / Yes
√ Would you like to use ESLint with this project? ... No / Yes
√ Would you like to use Tailwind CSS with this project? ... No / Yes
√ Would you like to use `src/` directory with this project? ... No / Yes
√ Use App Router (recommended)? ... No / Yes
√ Would you like to customize the default import alias? ... No / Yes
Creating a new Next.js app in PATH\TO\PROJECT\SAMPLEAPP.
Using yarn.
Initializing project with template: app
## 略
対話式でアプリの設定を訊かれるので答えて初期設定を済ませます。折角なので TypeScript も入れましょう。 Rust を触ってガチガチに固められた型の世界も入門したので何とかなるでしょう。
> yarn run dev
開発中はこれでホットリロードしつつ開発とデバッグを繰り返します。
> yarn run build
最後はビルド。 Yarn が分かっていて package.json を読めば何とかなります。
今回の成果物は Markdown の文章を一定のフォーマットに合わせて変換する簡易的な変換ツールです。オリジナルと変換後出力先の Textarea があって、ボタンクリック時に変換するという至ってシンプルなもの。
それゆえ Redux はいらないと踏んでいたのですが作り方的にコンポーネントを細分化しすぎて State 管理が思ったよりも煩雑になってしまったのが反省点。
メモ
幾つか躓いたりした部分のメモ。
TypeScript の構文
function OriginalForm(props: { name: string, head: string, muted: string }) {
// 略
}
const inputRef = useRef<HTMLTextAreaElement | null>(null);
const handleClickSend = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
// 略
}
この辺りが割と使った表現。配列やオブジェクトの要素の型を指定するときが <> で括る形式ですね。 Rust もそういえばこの構文でしたね。
日本語入力
島嶼は onChange で実装しようとしたのですが、日本語入力で躓いてしまった(変換できない)ので Button コンポーネントと onClick に方針転換。今回は限られた時間でサクッと作りたかったので根本的な解決と言うより回避ですね。
ref
import { useContext, useRef } from 'react';
// 初期値を与えること。 null でOK
const inputRef = useRef<HTMLTextAreaElement | null>(null);
// useContext
const orgText = useContext(OriginalText);
// click時
const handleClickSend = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault();
// 型として setText が string を受け付けるようにしているので null が渡らないように潰しておく
const text = typeof inputRef.current?.value === 'string' ? inputRef.current?.value : '';
orgText?.setText(text);
<Form.Control
as="textarea"
rows={10}
ref={inputRef}
defaultValue=""
/>
ref 及び useRef の使用。
プロパティ ‘name’ は型 ‘EventTarget’ に存在しません。
プロパティ ‘name’ は型 ‘EventTarget’ に存在しません。
というエラーメッセージ。
// 型として setText が string を受け付けるようにしているので null が渡らないように潰しておく
const text = typeof inputRef.current?.value === 'string' ? inputRef.current?.value : '';
必ず String 型が入るようにして回避。
You’re importing a component that needs useState. It only works in a Client Component but none of its parents are marked with “use client”, so they’re Server Components by default.
You’re importing a component that needs useState. It only works in a Client Component but none of its parents are marked with “use client”, so they’re Server Components by default.
'use client';
'use client'; を指定してクライアントサイドであることを教えればOK。
rning: You provided a value prop to a form field without an onChange handler. This will render a read-only field. If the field should be mutable use defaultValue. Otherwise, set either onChange or readOnly.
rning: You provided a
valueprop to a form field without anonChangehandler. This will render a read-only field. If the field should be mutable usedefaultValue. Otherwise, set eitheronChangeorreadOnly.
value 属性ではなく defaultValue 属性を指定する、か、 readOnly 属性を付ければOK。今回は出力先の表示専用の Textarea で読み取り専用で良かったので後者で回避。
build 時に Type ‘Context’ is not assignable to type ‘never’. 等のエラー
build 時に以下のエラーが発生。
Type error: Type 'OmitWithTag<typeof import("PATH/TO/PROJECT/src/app/page"), "metadata" | "default" | "config" | "generateStaticParams" | "revalidate" | "dynamic" | ... 4 more ... | "generateMetadata", "">' does not satisfy the constraint '{ [x: string]: never; }'.
Property 'OriginalText' is incompatible with index signature.
Type 'Context<never>' is not assignable to type 'never'.
これが最も嵌まった部分ですが、 page.tsx に createContext で export していたためと判明。
page.tsx は Next.js 上でエントリポイント的な特殊なページらしく(ドキュメントを読んでいない)、 export default function Home() {} 以外の export があるとマズい模様。ラッパーコンポーネントを用意して createContext するtsxを1つ下の子コンポーネントに機能を丸ごとスライドすることで回避。
確かに、冷静に考えれば初期状態である小文字始まりのコンポーネントっていかにも特殊な役割がありそうなモノですよね……。
静的サイトとして生成
Next.js 13.3 以降だとそれまでとはやり方が変わっており、しかも比較的最近のバージョンゆえネット上の記事が有効ではなかった部分(13.3以前のやり方の記事が多くヒットした)。
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
}
module.exports = nextConfig
next.config.js で nextConfig に output: 'export' を指定すればOK。出力先は out/ ディレクトリになります。
サブディレクトリにデプロイ
今回は本当にサクッとした静的サイトで、レンタルサーバのスペースで動かそうかとか考えていたレベルなので逆にデフォルトのドキュメントルートからの絶対パス指定だと引っかかってしまった部分。
まあ、アプリならば通常ドキュメントルートにデプロイしますよね、というところは本当そうだと思うのですが。
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
basePath: '/PATH/TO/PROJECT',
}
module.exports = nextConfig
next.config.js で nextConfig に basePath でドキュメントルートからのパスを指定すればOK。
ざっとこんなところで無事アプリを動かすことができました。日曜大工ならぬ日曜プログラミングで何とかなるレベルで良かった。
参考
Next.js
Google Fonts
React Bootstrap
- React-Bootstrap · React-Bootstrap Documentation
- Next.js で Bootstrap と React Bootstrap を使う|まくろぐ
- Grid: React-Bootstrap · React-Bootstrap Documentation
- css: React-Bootstrap · React-Bootstrap Documentation
yarn add bootstrapも必要import 'bootstrap/dist/css/bootstrap.min.css';も必要
- Form:
- Form controls | React Bootstrap
- 「ボタンクリック時に(ボタン以外の要素の)値を取得」というときは
useRefを使う
props
TypeScript の型の書き方
- Typescriptで学ぶReact 入門 Propsの渡し方と定義 – deve.K’s Programming Primer – プログラミング初心者のための入門ブログ
- TypeScriptの書き方講座-vol.1 | Happy Life Creators
- Typescript書き方の速成まとめ – Qiita
- 関数の型 – TypeScript Deep Dive 日本語版
Form
useState
useContext
- こんなに簡単なの?React Hook useContextでデータ共有 | アールエフェクト
- 【React】useContextの使い方 JavaScriptとTypeScriptの違いも | Almonta Blog
- React の Context の更新による不要な再レンダリングを防ぐ 〜useContext を利用した時に発生する不要な再レンダリングを防ぐ方法に関して〜 – Qiita
日本語入力
ref
- TypeScriptのもとでuseRefを使うときに知るべきRefObjectとMutableRefObjectについて
- 【React】ボタンクリックで別のDOM要素のイベントを起動する方法 | ramble – ランブル –
プロパティ ‘name’ は型 ‘EventTarget’ に存在しません。
プロパティ ‘name’ は型 ‘EventTarget’ に存在しません。
e.target, e.currentTarget
You’re importing a component that needs useState. It only works in a Client Component but none of its parents are marked with “use client”, so they’re Server Components by default.
You’re importing a component that needs useState. It only works in a Client Component but none of its parents are marked with “use client”, so they’re Server Components by default.
rning: You provided a value prop to a form field without an onChange handler. This will render a read-only field. If the field should be mutable use defaultValue. Otherwise, set either onChange or readOnly.
rning: You provided a
valueprop to a form field without anonChangehandler. This will render a read-only field. If the field should be mutable usedefaultValue. Otherwise, set eitheronChangeorreadOnly.
build 時に Type ‘Context’ is not assignable to type ‘never’. 等のエラー
build 時に以下のエラーが発生。
Type error: Type 'OmitWithTag<typeof import("PATH/TO/PROJECT/src/app/page"), "metadata" | "default" | "config" | "generateStaticParams" | "revalidate" | "dynamic" | ... 4 more ... | "generateMetadata", "">' does not satisfy the constraint '{ [x: string]: never; }'.
Property 'OriginalText' is incompatible with index signature.
Type 'Context<never>' is not assignable to type 'never'.
- never型 | TypeScript入門『サバイバルTypeScript』
- [React]ContextAPIのアンチパターン
- TypeScript をより安全に使うために その 3: オブジェクトの index signature とオプショナルなプロパティを避ける – Object.create(null)
- typescript – How to fix “Type ‘string’ is not assignable to type ‘never'”? – Stack Overflow
- reactjs – What is “not assignable to parameter of type never” error in TypeScript? – Stack Overflow
build, SSG
- NextJS 13.3 からStatic Exportsの方法が変わってた
- Deploying: Static Exports | Next.js
- Next.jsにおけるSSG(静的サイト生成)とISRについて(自分の)限界まで丁寧に説明する – Qiita
サブディレクトリへのデプロイ、 basePath
正規表現
- 最短一致: JavaScript: 正規表現での「最長一致」と「最短一致」の違い
- 複数行: 複数行にマッチさせる正規表現 | You Look Too Cool
- 後方参照: [JavaScript]String.matchとRegExp.execと後方参照 – chalcedony_htnの日記