第二週、私は APP のテーマ(実際には主に色)を簡単に定義し、APP のデータベースを構築し、データを使用して表示インターフェースを構築しました。
テーマシステム#
プロジェクトを初期化するとき、私は Tamagui を選びましたが、テーマシステムをカスタマイズし始めると、ドキュメントを見てめまいがしました😵💫。それに加えて、それが包括的なスタイルを持っているため、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 を選びました。理由は次のとおりです:
- 現在、フォローしているサーバーサイドも使用しており、多くのテーブル定義をコピーできます。
- コード生成を利用する Prisma などのライブラリよりも、テーブル定義を書くために ts を使用することが好きです。型を即座に更新できます。
- 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 をネイティブコンポーネントにレンダリングする方法を見てみましょう。
最後に、現在の状態を一緒に見てみましょう!