Open25

Next.jsアプリ開発メモ

brainvaderbrainvader

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アクション: 子アクターを停止するアクション
  • インライン・アクション
  • アクション・パラメータ

自己遷移とアクション

Modeling

ガード・オブジェクト

{
    guard: {
        type: 'guardFunc',
        params: { param1:  100 }
    }
}

状態マシン・アクター

状態遷移アクター

brainvaderbrainvader

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]

脚注
  1. Use-cases ↩︎

  2. Actor registration ↩︎

  3. Systems ↩︎

  4. Invoking and spawning actors ↩︎

brainvaderbrainvader

Day3

アクターの動的生成

Spawn

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}`
       })
    }))
  }
})
brainvaderbrainvader

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>
  );
}
脚注
  1. 事の良し悪しはあるでしょうが ↩︎

brainvaderbrainvader

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要素でも使えることです。普及すると便利なりそうですね。

brainvaderbrainvader

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>を使う。良く忘れるね。

brainvaderbrainvader

Day 9 textareaの値の更新はonChangeが必要の巻

onChangeがない場合の警告

ラインライムでダブル・クリックをしてdivからtextareaに切り替えてレンダリングをすると、以下のような警告を受けた。

brainvaderbrainvader

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はどうでしょうか。

Toolbar RadixUI

固定ツールバー

生HTMLとXSS

Undo/Redo

脚注
  1. イベント・ハンドラーを考えましょう ↩︎

  2. 実際の場合どうなのかは分かりませんが ↩︎

  3. あれ?特別なことやってないのでは・・・ ↩︎

brainvaderbrainvader

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で、三種類のタイプに分類されて説明されています。

  1. Standalone Application: 単一のアプリ
  2. Package-Based Repository: 複数パッケージで構成される
  3. 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側で決めてくれるのでいろいろ思い悩む必要もないというのも個人的にはいいと思います。

脚注
  1. WIPですが・・・ ↩︎

  2. Publishable and Buildable Nx Libraries ↩︎

brainvaderbrainvader

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

既存のプロジェクトへの導入

別々のレポジトリの統合

monorepoなので

既存のプロジェクトに導入できる。

プロジェクトの分離

モノレポなので、分離する全体はないのかなという気もします。packages.jsonが個別にあるケースでもlibsは共有されている可能性は高いので、forkして受け皿となるレポジトリを作り、相互にappを消す感じでしょうか?

消すことはできるので、別々に管理して関係性が生じたものから統合していくというのがいいのかもしれません。

一応nxコマンドにはremoveというサブコマンドがあります。

メモ

Decomposing a project using Nx - Part 1

brainvaderbrainvader

Day 13 AutoResizeフック

Scroll Heightとは?

scrollHeightプロパティはViewportに収まりきらなかったコンテンツを含む要素の高さです。textArea要素は通常入力に応じて拡縮しません。そのかわり垂直スクリール・バーが表示されます。これにより一部のコンテンツはオーバーフローとして隠されることになります。

AutoResizeは適宜この高さを調整して、全体が映るようにします。[1]

なお値は丸められるので、より正確な値が必要な場合はgetBoundingClientRect()を利用する必要があります。

自動リサイズ以外にも、規約等をきちんと読んだかをスクロールで判断して同意するような場合に利用されます。

Element: scrollHeight property

Reference

脚注
  1. ただし、ブラウザの表示領域であるビューポートを超えた場合はスクロールするしかないです ↩︎

brainvaderbrainvader

Day 14 shadcn/ui

prerequisite

  1. nxワークスペースの作成
  2. @nx/nextプラグインの追加
  3. アプリケーションの作成
  4. タグでスコープを設定(必要なら)

導入手順

管理下のアプリケーション・ディレクトリに移動して、

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

@nx/next:library

brainvaderbrainvader

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.

brainvaderbrainvader

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"
}
brainvaderbrainvader

ui alias

components.jsonのaliasesにはuiフィールドがあります。これは設定しなくても自動的にcomponents以下にuiディレクトリが作成されます。

 npx shadcn-ui@latest add button

を実行するとlibs/shadcn-ui/src/components/ui/button.tsxが作成されます。そのためおそらく設定しなくても大丈夫だと思います。

brainvaderbrainvader

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'
brainvaderbrainvader

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>

とすると解決した。なぜ?

brainvaderbrainvader

Day 17 エディタらしい体裁を整える。

bodyに100vhを指定すると親がスクリーン全体に拡大されるので、後は子要素をパーセンテージで分ければよい・・・のだと思う。

brainvaderbrainvader

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]

脚注
  1. Grouping Libraries ↩︎

brainvaderbrainvader

Day 20 ホワイトスペースの扱い

Textareaからpタグに切り替えるとテキストが空行の時pタグの高さが0になってしまう。

CSSにwhite-spaceというプロパティの設定で制御できた。

もう一つwhite-space-collapseがありこちらの方がいいかもしれない。