Hyoban

Hyoban

Don’t do what you should do, do you want.
x
github
email
telegram
follow

主題システム、ローカルファースト

第二週、私は APP のテーマ(実際には主に色)を簡単に定義し、APP のデータベースを構築し、データを使用して表示インターフェースを構築しました。

テーマシステム#

プロジェクトを初期化するとき、私は Tamagui を選びましたが、テーマシステムをカスタマイズし始めると、ドキュメントを見ているうちにめまいがしました😵‍💫。それに加えて、それが包括的なスタイルを持っているため、Unistylesに切り替えました。

Unistyles のテーマシステムは通常のオブジェクトです。私は Radix Color が大好きなので、それを渡すだけで十分です。Tailwind とは異なり、それぞれの色に対応するダークカラーが設計されており、ダークテーマのサポートが非常に簡単になります。

export const lightTheme = {
  colors: {
    ...accent,
    ...accentA,
  },
} as const

export const darkTheme = {
  colors: {
    ...accentDark,
    ...accentDarkA,
  },
} as const

多くの色を渡す必要があるため、対応するダークテーマにも対応するのを忘れることがありますが、型チェックを使用して制約を設けることができます。How to test your typesを参考にしてください。

type Expect<T extends true> = T
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? true : false
type _ExpectLightAndDarkThemesHaveSameKeys = Expect<Equal<
  keyof typeof lightTheme.colors,
  keyof typeof darkTheme.colors
>>

さらに、ランタイムを使用してテーマを簡単に変更できるため、次のような動的なテーマ切り替えを簡単に記述できます。

UnistylesRuntime.updateTheme(
  UnistylesRuntime.themeName,
  oldTheme => ({
    ...oldTheme,
    colors: {
      ...oldTheme.colors,
      ...accent,
      ...accentA,
      ...accentDark,
      ...accentDarkA,
    },
  }),
)

ローカルファースト#

ウェブページを作成する場合、ローカルファーストを行わない理由はありません。APP は SQLite を実行できる環境として、オフライン環境で開くことができます。現時点では、私の考えは、APP が主にローカルデータベースと対話し、データの同期にネットワークリクエストを使用することです。

技術スタックの選択に関しては、迷うことなく drizzle を選びました。理由は次のとおりです:

  1. 現在、フォローしているサーバーサイドも使用しており、多くのテーブル定義をコピーできます。
  2. コード生成を利用する Prisma などのライブラリよりも、テーブル定義を書くために ts を使用することが好きです。型を即座に更新できます。
  3. Expo 公式ドキュメント では、Expo SQLite の統合には drizzle が推奨されています。Prisma の統合 はまだ Early Access の段階です。

Expo SQLite は addDatabaseChangeListener のインターフェースを提供しており、最新のデータベースデータをリアルタイムで取得できます。drizzle は useLiveQuery を提供しています。ただし、現在のフックは useEffect の依存配列を正しく処理していません。

さらに、結果をキャッシュする必要があります。そうしないと、ページを切り替えるたびに不要なデータベースクエリが発生します。そのため、SWR を使用して独自のフックをラップします。

import { is, SQL, Subquery } from 'drizzle-orm'
import type { AnySQLiteSelect } from 'drizzle-orm/sqlite-core'
import { getTableConfig, getViewConfig, SQLiteTable, SQLiteView } from 'drizzle-orm/sqlite-core'
import { SQLiteRelationalQuery } from 'drizzle-orm/sqlite-core/query-builders/query'
import { addDatabaseChangeListener } from 'expo-sqlite/next'
import type { Key } from 'swr'
import type { SWRSubscriptionOptions } from 'swr/subscription'
import useSWRSubscription from 'swr/subscription'

export function useQuerySubscription<
  T extends
  | Pick<AnySQLiteSelect, '_' | 'then'>
  | SQLiteRelationalQuery<'sync', unknown>,
  SWRSubKey extends Key,
>(
  query: T,
  key: SWRSubKey,
) {
  function subscribe(_key: SWRSubKey, { next }: SWRSubscriptionOptions<Awaited<T>, any>) {
    const entity = is(query, SQLiteRelationalQuery)
    // @ts-expect-error
      ? query.table
      // @ts-expect-error
      : (query as AnySQLiteSelect).config.table

    if (is(entity, Subquery) || is(entity, SQL)) {
      next(new Error('Selecting from subqueries and SQL are not supported in useQuerySubscription'))
      return
    }

    query.then((data) => { next(undefined, data) })
      .catch((error) => { next(error) })

    let listener: ReturnType<typeof addDatabaseChangeListener> | undefined

    if (is(entity, SQLiteTable) || is(entity, SQLiteView)) {
      const config = is(entity, SQLiteTable) ? getTableConfig(entity) : getViewConfig(entity)

      let queryTimeout: NodeJS.Timeout | undefined
      listener = addDatabaseChangeListener(({ tableName }) => {
        if (config.name === tableName) {
          if (queryTimeout) {
            clearTimeout(queryTimeout)
          }
          queryTimeout = setTimeout(() => {
            query.then((data) => { next(undefined, data) })
              .catch((error) => { next(error) })
          }, 0)
        }
      })
    }

    return () => {
      listener?.remove()
    }
  }

  return useSWRSubscription<Awaited<T>, any, SWRSubKey>(
    key,
    subscribe as any,
  )
}

ここで queryTimeout に注意してください!!!。テーブルの変更は非常に頻繁に発生する可能性があるため、以前のクエリをキャンセルする必要があります。Drizzle はまだ AbortSignal を使用してクエリをキャンセルすることをサポートしていないため、setTimeout を使用して処理しています。

https://github.com/drizzle-team/drizzle-orm/issues/1602

OK、データをリクエストするときに正しいキーを設定するだけで、最新のデータを効率的に取得できます。プルダウンリフレッシュと定期的なデータ同期と組み合わせることで、APP は基本的なローカルファーストを実現できます。

次は何ですか?#

今週、私はフィードエントリの詳細を表示するために webview を使用しましたが、初回の読み込みが少し遅いように感じます。特別な最適化戦略が必要なのか、webview の制約がそのようなものなのかはわかりません。来週はネイティブコンポーネントに HTML をレンダリングする方法を見てみましょう。

最後に、現在の外観を一緒に見てみましょう!

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。