Next.jsアプリ開発メモ
Day 1
xstateのプロパティ
プロパティ | 説明 |
---|---|
id | 識別子 |
initial | 初期状態 |
states | 状態 |
context | 管理しているデータ |
on | イベントに対する対応 |
target | 遷移先の状態 |
after | 遅延遷移 |
always | イベント無し遷移 |
guard | 遷移時に満たすべき条件 |
invoke | 呼び出すアクター |
actions | アクション |
entry | 状態に遷移する時に実行するアクション |
exit | 別の状態に遷移する前に実行するアクション |
params | アクションに対する動的なパラメータ |
description | 状態遷移に関する記述 |
イベント
イベント・オブジェクト
{
type: `id.state`,
payload: `data`
}
状態遷移
{
target: `stateA`
description: `Write some text to explain the transition`
}
アクター
actor.send({type: "someEvent});
アクション
アクション・オブジェクト
{
action: {
type: `actionName`,
params: { param1: `hello`}
}
}
いろいろなアクション
- Assignアクション: コンテクストを更新するアクション
- Send-toアクション: アクターにイベントを送信するアクション
- Raiseアクション: 機械が自分自身に送信するアクション
- Enqueueアクション: 順序ありアクション
- Logアクション: メッセージをログに表示するアクション
- Cancelアクション: アクションを取り消すアクション
- Stop-childアクション: 子アクターを停止するアクション
- インライン・アクション
- アクション・パラメータ
自己遷移とアクション
ガード・オブジェクト
{
guard: {
type: 'guardFunc',
params: { param1: 100 }
}
}
状態マシン・アクター
状態遷移アクター
Day 2 todomvc-react
対象プロジェクト
サンプルプロジェクトのtodomvc-reactを読む
構成
- TodoMachine
- Todoコンポーネント
- TodosMachine
- Todosコンポーネント
TodoMachine
状態
statesプロパティは以下のようである。
- reading
- editing
- deleted
イベント
- CHANGE
- COMMIT
- BLUR
- CANCEL
マイグレーション
v4で書かれているのでv5にする必要がある。
input
v4までは動的にパラメータを変更して状態マシンを作るには、createTodoMachineのようなFactoryパターンが必要でした。
export const createTodoMachine = ({ id, title, completed }) =>
createMachine(
{
// config
}
)
inputを使う場合createActorの代に引数として渡します。[1]
const feedbackActor = createActor(feedbackMachine, {
input: {
userId: '123',
defaultRating: 5,
},
});
withconfig
withconfigはprovideに置き換わっている.
sendParent
Reference actors anywhereによると, 暗黙的なアクター・システムが構築されることから明示的にsendParentのような関数を使う必要はないようです。
代わりにreceptionist patternと呼ばれる方法でアクター同士の通信を行います。systemIdを指定することで通信相手を指定することができます。
import { createActor } from 'xstate';
const actor = createActor(machine, {
systemId: 'root-id',
});
actor.start();
invokeやspawn関数の引数として指定することもできます。[2]
systemはどうやって取得するのでしょうか?
The system can be accessed from the actor.system property of actors, and from the destructured { system } property from state machine actions:[3]
アクションの引数としてsystemを取得することができるようです。
const shippingMachine = createMachine({
// ...
on: {
'address.updated': {
actions: sendTo(({ system }) => system.get('notifier'), {
type: 'notify',
message: 'Shipping address updated',
}),
},
},
});
import { createMachine, createActor } from 'xstate';
const machine = createMachine({
entry: ({ system }) => {
// ...
},
});
const actor = createActor(machine).start();
actor.system;
また以下のような説明があります。
An example of the difference between invoking and spawning actors could occur in a todo app. When loading todos, a loadTodos actor would be an invoked actor; it represents a single state-based task. In comparison, each of the todos can themselves be spawned actors, and there can be a dynamic number of these actors.[4]
Day3
アクターの動的生成
const childMachine = createMachine({
/* ... */
});
const parentMachine = createMachine({
entry: [
assign({
childMachineRefs: ({ spawn }) => [
spawn(childMachine),
spawn(childMachine),
spawn(childMachine),
],
}),
],
});
のようにrefは配列でも良い。contextはイミュータブルなので、新しい配列として置き換える。
// なんか違う気がする・・・
actions: assign({
todosRef: ({ context, spawn }) => {
return context.todosRef.map((todo) => ({
..todo,
spawn(blockMachine, {
id: `block-${context.blocks.length}`,
input: {
id: context.blocks.length,
contenxt: "",
},
systemId: `block-${context.blocks.length}`
})
}))
}
})
Day 4 Reactとの関係
Reactパッケージ
シンプルに以下の四つの関数が提供されるだけです。
- useActor
- useMachine
- useActorRef
- useSelector
- createActorContext
createActorContext
コンテクストによって親子関係をあまり感じずに処理を書けるようになります。[1]
useActorRefフックがcontextから取得できます。この参照に対してsendでイベントを送信すれば親に対して通信ができるます。
import { SomeMachineContext } from '../path/to/SomeMachineContext';
function SomeComponent() {
const count = SomeMachineContext.useSelector((state) => state.context.count);
const someActorRef = SomeMachineContext.useActorRef();
return (
<div>
<p>Count: {count}</p>
<button onClick={() => someActorRef.send({ type: 'inc' })}>
Increment
</button>
</div>
);
}
-
事の良し悪しはあるでしょうが ↩︎
Day 6 ブロック・エディタにふさわっしい要素
input/textarea vs div
input/textareaの方がセマンティックスに関しては妥当だと思われます。ただ表示状態と編集状態を切り替えることを考える必要があります。
編集状態はinput/textareaでしょうが、表示状態コンテンツに応じて変える必要があります。 例えばコンテンツがコードならシンタックス・ハイライト済みの要素に切り替える必要がありますし、数式ならMathMLにすべきです。
とはいえdiv要素のcontenteditable属性を有効にする方が一般的です。Editor.jsやQuillもdiv要素を使っているので、これに倣います。
Web フォントを使って contenteditable から脱出する
と思ったのですが仕様上ブロックの種類の指定などいろいろUIが必要なため、textareaとdivを組み合わせることにしました。
EditContext API
編集可能なテキスト領域を表示できるAPIのようです。まだまだこれからって感じです。Firefoxも現時点ではサポートしていません。驚くのはCanvas要素でも使えることです。普及すると便利なりそうですね。
Day7
formとtextarea
自動的に保存するにしても、最終的にformのsubmitボタンで保存するってのはどうなのだろうか?
autoresize textarea
Building an Autosizing Textarea in React (Code Included)
React + TypeScript: Create an Autosize Textarea from scratch
Controlled and uncontrolled components
Day 8 onDoubleClickはダメの巻
onDoubleClickによる警告
Markuplintから以下のような警告を受けた。
onDoubleClickというのは使わずにonClickハンドラーの中で, クリックの回数ごとに処理を分岐させるのが良いらしい。
onClick works but onDoubleClick is ignored on React component
function clickHandler(event: MouseEvent<HTMLDivElement>) {
const clickCount = event.detail
if (clickCount === 2) {
send(Events.DOUBLE_CLICK)
}
}
MouseEvent<T>
イベントの型はMouseEvent<T>を使う。良く忘れるね。
Day 9 textareaの値の更新はonChangeが必要の巻
onChangeがない場合の警告
ラインライムでダブル・クリックをしてdivからtextareaに切り替えてレンダリングをすると、以下のような警告を受けた。
Day 10 編集状態と表示状態の見た目を合わせるの巻
textareaが親要素を超えてしまう
textareaのautosize
autosizeとかautograwingとかいう。
Headless化
現状必要性は低いがどんな感じかだけ雰囲気を掴んでおく。
The complete guide to building headless interface components in React
render propsによる方法
そもそもrender propsって何ぞや?
Calling a render prop to customize rendering
LogRocketの記事ではchildrenとして渡しているが, 元々はrenderというプロパティに描画用の関数を渡して制御しようという考えのようです。CountdownはHeadless化されると描画方法を知りませんので、外部から制御する必要があります。
Reactの基本として親コンポーネントは子コンポーネントに制御関数[1]を渡せますので、子コンポーネントを受け取りそこに自分の提供するAPIを渡す形で制御と描画を分離します。
children等の渡され方
何となくどうだったか良く分からなくなったので書いておくと,
props.children
として子コンポーネントからは取得できます。つまりpropsの特殊なものと考えればいいと思います。
Hookによる方法
ざっくりとはカスタム・フックとして制御関数を返すようにすれば良いじゃないかということです。こうしてみるとフックは非常にシンプルで強力なパターンだなと思いました。
まとめ
なんだかヘッドレス化のやり方というよりは、単なるフックの導入という感じでした。Reactに関してはヘッドレス化はカスタム・フックに制御をまとめると考えればいいのかもしれません。TanStack Tableはどちらかというとtableモデルを返すような表現になっていました。MVCパターンと違って、Controllerに記述する描画の指示はReact内部に隠されています。後はモデルにいろんな制御関数を持たせて、ビューと結合させるというのは直観的で分かりやすいメンタルモデルだと思いました。[2]
つまりtextareaの仕様を元にモデル化をすればいいわけです。[3]
メモ
Toolbar
iconが欲しいというのがまずあり、スタイルがないというのでRadixUIはどうでしょうか。
固定ツールバー
生HTMLとXSS
Undo/Redo
Day 11 プロジェクトの管理方法
libraryの構築
- Create a Simple React Component Library with Vite
- Create a Component Library Fast🚀(using Vite's library mode)
- React Native with Typescript + Web (Vite.js) + Storybook
monorepo
- Nx and Turborepo
- Building a full-stack TypeScript application with Turborepo
- zeno
- 今モノレポやるならどのツール使うのがいいのん??
- Nx で shadcn/ui
Nx
Day 12 Nx
Nxって何?
ホームページには以下のような説明があります。
Smart Monorepos・Fast CI
Nx is a build system with built-in tooling and advanced CI capabilities . It helps you maintain and scale monorepos, both locally and on CI.
多くのケースではnpmなどJavaScript/TypeScriptのコード・ベースを前提にしているでしょうが、ここだけ見ると特に言語の制約などはないようです。
Tuborepoでは
Turborepo is an intelligent build system optimized for JavaScript and TypeScript codebases.
とあるので他言語で書いたプロジェクトを含む場合は考慮されていないのかもしれません。
Plugins
とはいえプラグイン一覧にはcargoとかgoという言葉も見られるので、多言語であってもプラグインが用意できればNxで制御できそうな感じがします。
Getting Started with Pluginsには、
Nx plugins contain generators and executors that extend the capabilities of an Nx workspace. They can be shared as npm packages or referenced locally within the same repo.
とあります。
Executor
エクスキューターはCLIを呼び出すことができます。Local Executorsではechoコマンドを呼び出すエクスキューターを定義しています。
nxrs/cargoはcargoコマンドを実行できるようにするプラグインです。[1]
Generators
プロジェクトへのコード・ファイルの生成などができるようです。公開ライブラリのためのコードなんかもジェネレーターの機能で実現しているようです。
The --buildable and --publishable options are available on the Nx library generators for the following plugins:[2]
どんな感じでプロジェクトを管理するのか?
Integrated Repos vs. Package-Based Repos vs. Standalone Appsで、三種類のタイプに分類されて説明されています。
- Standalone Application: 単一のアプリ
- Package-Based Repository: 複数パッケージで構成される
- Integrated Repository: バージョンなどもリポジトリで統一
1のケースはツール類だけNxが提供するものを利用するケースです。
In fact, many developers use Nx not primarily for its monorepo support, but for its tooling support, particularly its ability to modularize a codebase and, thus, better scale it.
ただしこの場合は
It is like an integrated monorepo setup, but with just a single, root-level application.
とありintegrated monorepoと同じような設定になるそうです。アプリ一つなので当然という感じもしますが、後でいろいろ追加するというケースは自然なのでその時にひと手間いりそうな匂いがします。
2と3はnpmならnode_modulesが共有されるかという違いのようです。
Build tools like Jest and Webpack work as usual, since everything is resolved as if each package was in a separate repo and all of its dependencies were published to npm. Moving an existing package into a package-based repo is very easy since you generally leave that package's existing build tooling untouched. Creating a new package inside the repo is just as difficult as spinning up a new repo since you have to create all the build tooling from scratch.
Applications and libraries
Applications and librariesによると、
Each Nx library has a so-called "public API", represented by an index.ts barrel file. This forces developers into an "API thinking" of what should be exposed and thus be made available for others to consume, and what on the others side should remain private within the library itself.
とあります。APIのようにライブラリとして積極的に分ける方がNxの自然な使い方のようです。appsとlibsは2:8ぐらいの割合が良いとも書いてあります。
As such, if we follow a 80/20 approach:
- place 80% of your logic into the libs/ folder
- and 20% into apps/
Publishable and Buildable Nx Libraries
次の心配はライブラリと聞くとnpmとかで公開するためのものなのか?とか勝手にそうなってしまわないかという心配があると思います。
それは杞憂で公式でも強調されています。
Developers new to Nx are initially often hesitant to move their logic into libraries, because they assume it implies that those libraries need to be general purpose and shareable across applications.
通常Nxでlibsとは単なるコードの分割と変わりません。もちろん公開したりすることもできます。 そのための制御にはpublishableとかbuildableといったフラグを利用します。
Keep in mind that the --publishable flag does not enable automatic publishing. Rather it adds to your Nx workspace library a builder target that compiles and bundles your app. The resulting artifact will be ready to be published to some registry (e.g. npm).
とあるので、単純にこのフラグを有効にしてビルドすると勝手に公開されるというわけでもないようです。あくまでnpmなどの外部レジストリへの公開はオプションと考えればよいでしょう。
## まとめ
Nxは単一のアプリの管理であっても便利そうです。ディレクトリの構成などもNx側で決めてくれるのでいろいろ思い悩む必要もないというのも個人的にはいいと思います。
-
WIPですが・・・ ↩︎
Day 12 Nxで気になるポイント
事例から
フロントエンドの Monorepo をやめてリポジトリ分割したワケを読むとモノレポ管理ツールが必要とする機能が見えてきそうです。
先ほどご紹介したようにカミナシのプロダクトは React をベースにしていますが、React のバージョンは web と mobile で密な依存関係にあり、Expo SDK のアップグレードのために mobile だけ React のバージョンを上げるということが難しい状況でした(Monorepo 構成で複数の React を共存させることができないため *1)。
この依存関係は本来 Monorepo のメリットでもありますが、裏を返すとどちらか片方の変更がもう片方にも影響を与えてしまうというデメリットにもなり得ます。
また、あるパッケージで使用しているライブラリを別のパッケージから利用できてしまうため、暗示的な依存関係も生まれていました。
ReactとReact Nativeで依存関係が密結合になっているというのはおそらくpackage-based repository
なら解決できるのかなと思う。
次は依存関係について。
Dependency Management Strategies
Enforce Module Boundaries
GitHubとの相性
Deploying Multiple Apps From a Monorepo to GitHub Pages
既存のプロジェクトへの導入
- Adding Nx to your Existing Project
- Preserving Git Histories when Migrating other Projects to your Nx Workspace
- Manual migration of existing code bases
別々のレポジトリの統合
monorepoなので
既存のプロジェクトに導入できる。
プロジェクトの分離
モノレポなので、分離する全体はないのかなという気もします。packages.jsonが個別にあるケースでもlibsは共有されている可能性は高いので、forkして受け皿となるレポジトリを作り、相互にappを消す感じでしょうか?
消すことはできるので、別々に管理して関係性が生じたものから統合していくというのがいいのかもしれません。
一応nxコマンドにはremoveというサブコマンドがあります。
メモ
Day 13 AutoResizeフック
Scroll Heightとは?
scrollHeightプロパティはViewportに収まりきらなかったコンテンツを含む要素の高さです。textArea要素は通常入力に応じて拡縮しません。そのかわり垂直スクリール・バーが表示されます。これにより一部のコンテンツはオーバーフローとして隠されることになります。
AutoResizeは適宜この高さを調整して、全体が映るようにします。[1]
なお値は丸められるので、より正確な値が必要な場合はgetBoundingClientRect()を利用する必要があります。
自動リサイズ以外にも、規約等をきちんと読んだかをスクロールで判断して同意するような場合に利用されます。
Element: scrollHeight property
Reference
- How to Dynamically Adjust the Height of a Textarea in ReactJS
- React + TypeScript: Create an Autosize Textarea from scratch
- TextareaAutosize.tsx
- How to Dynamically Adjust the Height of a Textarea in ReactJS
-
ただし、ブラウザの表示領域であるビューポートを超えた場合はスクロールするしかないです ↩︎
Day 14 shadcn/ui
prerequisite
- nxワークスペースの作成
- @nx/nextプラグインの追加
- アプリケーションの作成
- タグでスコープを設定(必要なら)
導入手順
管理下のアプリケーション・ディレクトリに移動して、
npx shadcn-ui@latest init
ではダメらしい。基本的にはManual Installationを参考にすればよい。
違う点
Tailwindの設定
Using Tailwind CSS in React and Next.jsによるとTailwindの導入に関しては、
nx g @nx/next:setup-tailwind --project=gaku-note
とすればよいらしいです。
ライブラリの作成
ライブラリはNx独自なので当然作る必要があります。How to Use shadcn with Nx Next.jsを参考にすると、以下のようになります。
npx nx generate @nx/next:library shadcn-ui --importPath=@libs/shadcn-ui --appProject=gaku-note
となる。ただTauriのデスクトップ版でも使うことを考えるとwebという名前を付けるのはおかしいのかなと思います。tagを使うとlintでエラーを吐けるのでディレクトリ名は素直にいきたいと思います。
npx nx generate @nx/next:library shadcn-ui --directory=libs/shadcn-ui --importPath=@libs/shadcn-ui --dry-run
importPathオプションについて
Nxライブラリを作ろうとして以下のコマンドを実行します。
npx nx generate @nx/next:library shadcn-ui --directory=libs/shadcn-ui --importPath=@libs/sha
dcn-ui --dry-run
すると@libs/shadcn-uiというパスはすでにあると表示されます。
NX You already have a library using the import path "@libs/shadcn-ui". Make sure to specify a unique one.
dry-runオプションなしで実行した場合、確かにtsconfig.base.jsonのpathsフィールドに自動的に@libs/shadcn-uiの設定が追記されます。ただこれはlibs/shadcn-ui/src/server.tsへのパスだったりします。
shadcn-ui-nx-next/tsconfig.base.jsonでは以下のように設定されています。
"paths": {
"@libs/shadcn-ui": ["libs/shadcn-ui/src/index.ts"],
"@libs/shadcn-ui/*": ["libs/shadcn-ui/src/*"]
}
そしてパスはcomponents.jsonのaliasesフィールドで参照されるようです。
The CLI uses these values and the paths config from your tsconfig.json or jsconfig.json file to place generated components in the correct location.
Path aliases have to be set up in your tsconfig.json or jsconfig.json file.
tsconfig.jsonがない
nxワークスペースを作った場合ルート直下にはtsconfig.base.jsonが作られます。この状態でコンポーネントを追加してみます。
npx shadcn-ui@latest add button
この場合tsconfig.jsonがないと警告を受けます。
Failed to load tsconfig.json. Couldn't find tsconfig.json
次に空のtsconfig.jsonを追加して実行してみます。@libsというディレクトリが作成されます。これは意図している動作とは違います。pathsが参照できる必要があるようです。tsconfig.base.jsonを拡張すればよいでしょう。
{
"extends": "./tsconfig.base.json"
}
ui alias
components.jsonのaliasesにはuiフィールドがあります。これは設定しなくても自動的にcomponents以下にuiディレクトリが作成されます。
npx shadcn-ui@latest add button
を実行するとlibs/shadcn-ui/src/components/ui/button.tsxが作成されます。そのためおそらく設定しなくても大丈夫だと思います。
Nx
npx nx run o-chara:dev
を実行するとtsconfig.jsonのincludeフィールドに.next/types/**/*.tsが追加されます。
We detected TypeScript in your project and reconfigured your tsconfig.json file for you. Strict-mode is set to false by default.
The following suggested values were added to your tsconfig.json. These values can be changed to fit your project's needs:- include was updated to add '.next/types/**/*.ts'
Day 16 suppressHydrationWarning
Next.jsでtheme-providerを挿入した。suppressHydrationWarningを設定するのを忘れたら警告を食らった。
Text content does not match server-rendered HTML
Warning: Extra attributes from the server: style,class
html
とりあえず
<html lang="en" suppressHydrationWarning>
とすると解決した。なぜ?
Day 17 エディタらしい体裁を整える。
bodyに100vhを指定すると親がスクリーン全体に拡大されるので、後は子要素をパーセンテージで分ければよい・・・のだと思う。
Day 18 Shadcn-uiでTreeコンポーネントの実装方法
Day 19 ディレクトリの名前
バックエンドのAPIに対する通信はdata-accessというディレクトリにまとめる。クライアントサイドの状態管理に関してはsrc/lib/+stateというディレクトリに置く。
sharedライブラリには複数のアプリケーションで共有されるコードを置く。
Let's use Nrwl Airlines as an example organization. This organization has two apps, booking and check-in. In the Nx workspace, libraries related to booking are grouped under a libs/booking folder, libraries related to check-in are grouped under a libs/check-in folder and libraries used in both applications are placed in libs/shared.[1]
Day 20 ホワイトスペースの扱い
Textareaからpタグに切り替えるとテキストが空行の時pタグの高さが0になってしまう。
CSSにwhite-spaceというプロパティの設定で制御できた。
もう一つwhite-space-collapseがありこちらの方がいいかもしれない。