🙆‍♀️

signiaを使ったワークアラウンドの紹介

2024/09/02に公開

はじめに

Signiaライブラリは状態管理ライブラリの一つであり、legend-stateなどと同じ種類のものとなります。この記事では、状態管理における情報設計の観点に着目して、Signiaライブラリを使った簡単なワークアラウンドを紹介します。状態管理をクラス化できるアプローチを公式ドキュメントで訴求している部分について展開できればと存じます。

ターゲット読者

Signiaやリアクティブプログラミングに興味があるフロントエンドエンジニア。

Signiaの基本概念

Signiaは、TypeScript向けの最小限で高速かつスケーラブルなシグナルライブラリです。特定のフレームワークに依存せず、公式のReactバインディングも提供されています。もともとtldrawのために開発され、他のリアクティブシグナルライブラリでは対応できないパフォーマンス要求を満たすために作られました。

Signiaでは、アプリケーションの状態管理に使う「シグナル」には2つの種類があります。

シグナル 説明
Atoms アプリケーションのルート状態を格納する
Computed Signals 他のシグナルから派生した計算結果を保持する

ここで、公式ドキュメントをもとに、atomを使ったState管理について簡単に説明します。

atomの作成

firstNameというAtomを作成し、初期値を「David」と設定するコードになります。

名前はデバッグを容易にするために必要ですが、ユニークである必要はありません。

import { atom } from 'signia'

const firstName = atom('firstName', 'David')

atomの更新

setメソッドを使った直接更新とupdateメソッドを使った関数による更新の2つがあります。

firstName.set('John')
console.log(firstName.value) // John

firstName.update((value) => value.toUpperCase())
console.log(firstName.value) // JOHN

Computed Signalsの作成と利用

computed関数を使って、他のシグナルから派生するシグナルを作成できます。

import { computed, atom } from 'signia'

const firstName = atom('firstName', 'David')
const lastName = atom('lastName', 'Bowie')

const fullName = computed('fullName', () => {
    return `${firstName.value} ${lastName.value}`
})

console.log(fullName.value) // David Bowie

Computed Signalsは直接更新できないため、元となるAtomを更新することで自動的に計算結果を更新できます。

firstName.set('John')
console.log(fullName.value) // John Bowie

クラスを使ったコードの整理方法

クラスで構造化するにあたり、以下のメリットがあることを訴求しています。

トピック 説明
ミューテーション関数のグループ化 状態を変更する関数をクラス内にまとめることで、コードの整理と可読性を向上
派生データの共有 クラスを使って派生データを一元管理し、複数のコンポーネント間で共有する
ライフサイクル管理 クラスを使って状態管理のライフサイクル(初期化、破棄など)を統一的に扱う
Atomのプライベート化 クラス内でAtomをプライベートとして扱うことで、状態の変更をクラス内に限定し、コードの乱雑さを防ぐ

ここで、少し応用したデモを用意して解説してみます。

デモの内容としては文字数カウンターとなります。入力した文字を監視して、その文字数をリアクティブに表示するといったものになります。

WordCounterManagerの実装

Signiaのシグナルを活用して、アプリケーションの状態管理を効率的に行うために、今回はWordCounterManagerクラスを実装していきます。このクラスは、テキストの管理と、そのテキストに基づく文字数カウントを行う役割を担います。

クラスの設計方針

1. 状態の管理
textという文字列を格納するためのatomを内部的に持ちます。このatomは、テキストエディタの現在の内容を保持します。

2. 派生データの共有
テキストの内容から計算される文字数(wordCount)を、クラスのプロパティとして定義します。これにより、文字数カウントのロジックがクラスにカプセル化され、外部のコンポーネントが簡単にアクセスできるようになります。

3. ミューテーションの管理
テキストを更新するためのsetTextメソッドを提供します。このメソッドを通じてのみatomを更新することで、状態管理が一元化され、コードベースが整理されます。

それでは、具体的なWordCounterManagerクラスの実装を見ていきましょう。

import { atom } from 'signia'

type State = {
  text: string
}

export class WordCounterManager {
  // プライベートなatomを定義し、初期値として空の文字列を設定します
  private readonly _state = atom<State>('useWordCounter', {
    text: '',
  } satisfies State)

  // テキストの状態を取得するためのゲッター
  get state() {
    return this._state.value
  }

  // テキストの文字数を計算して返すゲッター
  get wordCount() {
    return this._state.value.text.length
  }

  // テキストを更新するためのメソッド
  setText = (value: string) => {
    this._state.update((state) => {
      return {
        ...state,
        text: value,
      } satisfies State
    })
  }
}

1. _stateの定義
atomを使って、textを格納するための_stateを定義しています。この_stateはクラス内でプライベートに扱われ、外部から直接アクセスできないようにしています。

2. stateゲッター
stateゲッターを通じて、現在のテキストの状態を取得できます。このプロパティは、クラス外からテキストの内容を確認するために使用されます。

3. wordCountゲッター
テキストの長さを計算し、その値を返すゲッターです。これにより、文字数カウントのロジックがクラスにカプセル化され、他のコンポーネントから簡単に利用できます。

4. setTextメソッド
setTextメソッドを使ってテキストを更新します。このメソッドを通じてのみ、テキストの内容が変更されるため、状態管理の一貫性が保たれます。

WordCounterManagerクラスの実装により、テキストの状態とその派生データ(文字数カウント)が効率的に管理されます。このようにクラスを使って状態管理のロジックを整理することで、コードベースの可読性と保守性が向上します。次に、このクラスをReactコンポーネントでどのように利用するかを見ていきましょう。

コンテキストを使った依存関係の管理

Reactアプリケーションでは、コンポーネント間での状態共有や依存関係の管理が重要です。小規模なアプリケーションでは、単純なプロップスの受け渡しで対応できることもありますが、アプリケーションが複雑になるにつれて、状態管理のロジックが分散しやすくなります。そこで、Reactのコンテキスト(Context)を活用することで、依存関係の管理をシンプルかつ効率的に行う方法を紹介します。

まず、WordCounterManagerクラスのインスタンスをコンテキストとして提供するためのWordCounterManagerContextを作成します。

import { createContext, useContext, useMemo } from 'react'
import { WordCounterManager } from '@/features/wordCounter/core/WordCounterManager'

type WordCounterManagerContextProps = {
  wordCounterManager: WordCounterManager
}

const WordCounterManagerContext = createContext<WordCounterManagerContextProps>({} as WordCounterManagerContextProps)

次に、WordCounterManagerを初期化し、コンポーネントツリー全体に提供するためのWordCounterProviderコンポーネントを実装します。

type Props = {
  children: React.ReactNode
}

export const WordCounterProvider = ({ children }: Props) => {
  const initialValues = useMemo<WordCounterManagerContextProps>(
    () => ({
      wordCounterManager: new WordCounterManager(),
    }),
    []
  )

  return (
    <WordCounterManagerContext.Provider value={initialValues}>
      {children}
    </WordCounterManagerContext.Provider>
  )
}

最後に、他のコンポーネントでWordCounterManagerを利用するためのuseWordCounterManagerフックを実装します。

export const useWordCounterManager = () => useContext(WordCounterManagerContext)

このフックを使うことで、任意のコンポーネントからWordCounterManagerのメソッドやプロパティにアクセスできます。例えば、テキストフィールドの内容を更新する際に、このフックを使ってWordCounterManagersetTextメソッドを呼び出すことができます。

WordCounterコンポーネントの実装

上記プロバイダーでラップしつつ、利用側ではフック経由で入力テキストをステート管理し始めます。

export function Demo() {
  return (
    <WordCounterProvider>
      <WordCounter />
      <DisplayWordCount />
    </WordCounterProvider>
  )
}
export function WordCounter() {
  const {
    wordCounterManager: { setText },
  } = useWordCounterManager()

  return (
    <Box>
      <TextField
        autoFocus={true}
        onChange={(e) => {
          setText(e.currentTarget.value)
        }}
      />
    </Box>
  )
}

DisplayWordCountコンポーネントの実装

WordCounterManagerwordCountプロパティを使って文字数を監視し、変化があれば即座にUIを更新します。

export const DisplayWordCount = () => {
  const {
    wordCounterManager: { wordCount },
  } = useWordCounterManager()

  return (
    <Box>
      <Typography>{wordCount}文字</Typography>
    </Box>
  )
}

ワークアラウンドの利点と考察

今回のブログでは、Signiaを使ったワークアラウンドとして、WordCounterManagerを中心としたテキスト管理と文字数カウント機能を実装しました。このワークアラウンドには、いくつかの重要な利点があり、それらがアプリケーション開発にどのように役立つかを考察します。

1. 状態管理の一元化とコードの整理

Signiaのatomを使い、WordCounterManagerクラス内に状態をカプセル化することで、状態管理の一元化が可能になります。これにより、状態が散在することなく、特定のクラスやコンポーネントに集約されるため、コードが整理され、可読性が向上します。

2. リアクティブなUIの構築

Signiaのシグナルを活用することで、ユーザーの操作に応じて自動的にUIを更新するリアクティブなUIを簡単に構築できます。今回の例では、テキストの入力に応じて文字数がリアルタイムで更新される機能を実現しました。

3.フレームワークに依存しない柔軟性

Signiaはフレームワークに依存しないライブラリであり、React以外のフレームワークでも使用できます。このフレームワーク非依存性により、Signiaをさまざまなプロジェクトで柔軟に活用できる点は大きな強みです。

4. パフォーマンスの向上

Signiaはもともとtldrawの高いパフォーマンス要求に応えるために開発されたライブラリであり、その軽量かつ高速なシグナル処理により、パフォーマンスの向上が期待できます。

5. コードの拡張性と保守性

クラスを使ってロジックを整理し、コンテキストを通じて状態を共有することで、コードの拡張性と保守性が高まります。新たな機能を追加する際も、既存のロジックに影響を与えずに、簡単に拡張できます。

おわりに

今回のワークアラウンドを通じて、Signiaを活用した状態管理の有効性が確認できました。特に、リアクティブなUIの実現やパフォーマンス向上に寄与する点が大きな魅力です。また、フレームワークに依存しない柔軟性と、コードの整理や保守性向上のためのクラス設計が、アプリケーション開発において重要な要素であることが再確認できました。

Signiaは、特に高パフォーマンスが求められるリアルタイムアプリケーションに最適ですが、汎用性が高いため、さまざまなプロジェクトで活用できるポテンシャルを秘めています。今後もこのようなワークアラウンドを通じて、Signiaのさらなる可能性を探求し、より高度なアプリケーションを構築していきましょう。

補遺

最後にデモコードになります。

HITOTSU株式会社 テックブログ

Discussion