👏

React と React Native 両対応のUIライブラリ Tamagui の紹介

2023/01/07に公開約9,200字2件のコメント

はじめに

以前 React Native について書いた記事 (現在非公開) のコメントで kiyomizu さんより、Web と React Native 両方に対応した UI フレームワーク Tamagui というのもがあると情報をいただきましたので、とても気になり調べてみました。

Tamagui とは

https://tamagui.dev/

React と React Native の両方で動作する UI ライブラリで、Web とネイティブの間でより多くのコードを共有することを目的に作成されました。JSXStyle のフォークから始まったプロジェクトで、Vercel の Nate さんが開発をしています。

2022 年 12 月 30 日に Version 1.0 がリリースされました。

とりあえずコードを見てみる

百聞は意見に如かずということでまず基本のコードを見てみます。

import { Stack, Text } from '@tamagui/core';

const Hello = () => {
  return (
    <Stack margin={10}>
      <Text>Hello</Text>
    </Stack>
  );
};

Stack Text は Tamagui のベースとなるビューであり、 React Native の View Text と同等です。Web の場合はデフォルトで div span へ変換されます。h1 など指定することもできます。

ここで1点気にになる箇所があります。React Native の View では下記コードのように style プロパティでスタイリングしますが、Stackmargin={10} のようにインラインスタイルを使用しています。View でインラインスタイルは使用できないはずです。

import { View, Text } from 'react-native';

const Hello = () => {
  return (
    <View style={{ margin: 10 }}>
      <Text>Hello</Text>
    </View>
  );
};

これは Tamagui が React Native と React Native Web でサポートされている style プロパティのスーパーセットを使用して、StackText のプロパティにフラット化しているためです。このような実装にした理由を探してみましたが、公式サイトには見当たりませんでした。(2023年1月7日時点) リンクはあるけどまだ未記載っぽい?

styled を使用することで、既存のコンポーネントを拡張して新しいコンポーネントを作成することもできます。

import { Stack, styled } from '@tamagui/core'

export const Circle = styled(Stack, {
  borderRadius: 100_000_000,
  variants: {
    pin: {
      top: {
        position: 'absolute',
        top: 0,
      },
    },
    // 型付き変数 (boolean)
    centered: {
      true: {
        alignItems: 'center',
        justifyContent: 'center',
      },
    },
    // 型付き変数 (string)
    color: {
      ':string': (color) => {
        return {
          color,
          borderColor: color,
        }
      },
    },
    // 関数変数
    doubleMargin: (val: number) => ({
      margin: val * 2,
    }),
  } as const,
})

<Circle pin="top" centered color="#fff" doubleMargin={10} />

今回は第 1 引数に渡されている Stack コンポーネントを拡張し、Circle コンポーネントを定義しています 。Props で渡したい変数は variants で宣言します。Props の宣言方法にはご覧のようにいくつか種類があります。もちろん TypeScript の型チェックが効きます。

とりあえずコードを見てみましたが、特に難しい記述はなさそうで JavaScript を知っていればある程度は書けるかな、といった印象です。

構成

Tamagui は Tamagui Core、Tamagui Static、Tamagui UI の 3 つのパートで構成されているため、それぞれの役割を確認していきます。

Tamagui Core

React Native の API を 100%サポートする、依存性 0 の React Native / Web 用スタイルライブラリ。React Native と Tamagui Core は切り離されているため、Web のバンドルサイズに影響を与えることはありません。既に登場した、StackText styled などは Core に属します。

特徴

  • 27kb、依存性 0
  • Native と Web で 100%同じように動作する
  • SSR、React Server Component、Concurrent Mode を 100%サポート
  • 強力な型付き変数
  • 決定論的で推論しやすいスタイルマージ

Tamagui Static

フロントエンドの React に特化した、Babel と Node ベースの最適化コンパイラ。Webpack、Vite、Next、React Native、Expo のアダプタを持つプラグインを介して、Web とネイティブをサポートしています。ロジックが散りばめられたインラインスタイルもアトミック CSS と最小限の JS に変換します。また部分評価でコンポーネントツリーを平坦化して最小限の CSS を出力します。後に Tamagui Static がどのようにコンパイルするのか確認します。

Tamagui はブログなどをみる限り、コンパイラやパフォーマンスにかなり力を入れているように思えます。コンパイラの詳しい実装については公式サイトをご覧ください。

https://tamagui.dev/docs/intro/why-a-compiler

Tamagui UI

Core の上に構築されたコンポーネントキット。ButtonSelectDialog など使いやすいコンポーネントがたくさんあります。

特徴

  • すべてのコンポーネントが ネイティブ と Web で動作する
  • 自由にカスタマイズ可能
  • テーマ変更時に再レンダリングを回避

コンパイル

Tamagui Static がどのようにコンパイルするのか見てみます。

コンパイル前

YStack$5 については後に説明します。

app.tsx
import { Paragraph, YStack } from 'tamagui'

const App = (props) => (
  <YStack
    padding={props.big ? '$5' : '$3'}
    {...(props.colorful && {
      backgroundColor: 'green',
    })}
  >
    <Paragraph size="$2">
      Lorem ipsum dolor.
    </Paragraph>
  </YStack>
)

コンパイル後

app.js
const _cn5 = " _color-scmqyp _d-1471scf _ff-xeweqh _fs-7uzi8p _lineHeight-1l6ykvy"
const _cn4 = "  _bc-1542mo4"
const _cn3 = " _pb-12bic3x _pl-7ztw5e _pr-g6vdx7 _pt-1vq430g"
const _cn2 = " _pb-z3qxl0 _pl-14km6ah _pr-1qpq1qc _pt-1medp4i"
const _cn = " _d-6koalj _fd-eqz5dr _fs-1q142lx "
import { Paragraph, YStack } from 'tamagui'

const App = props => <div className={_cn + (props.big ? _cn2 : _cn3 + (" " + (props.colorful ? _cn4 : " ")))}>
    <span className={_cn5}>
      Lorem ipsum dolor.
    </span>
  </div>
app.css
._d-6koalj{display:flex;}
._fd-eqz5dr{flex-direction:column;}
._fs-1q142lx{flex-shrink:0;}
._pb-z3qxl0{padding-bottom:var(--space-5);}
._pl-14km6ah{padding-left:var(--space-5);}
._pr-1qpq1qc{padding-right:var(--space-5);}
._pt-1medp4i{padding-top:var(--space-5);}
._pb-12bic3x{padding-bottom:var(--space-3);}
._pl-7ztw5e{padding-left:var(--space-3);}
._pr-g6vdx7{padding-right:var(--space-3);}
._pt-1vq430g{padding-top:var(--space-3);}
._bc-1542mo4{background-color:rgba(0,128,0,1.00);}
._d-1471scf{display:inline;}
._fontFamily-xeweqh{font-family:var(--font-body);}
._fontSize-7uzi8p{font-size:var(--fontSize-2);}
._lineHeight-1l6ykvy{line-height:var(--lineHeight-2);}

Tamagui のパフォーマンス向上の鍵は、ビューのフラット化(ツリーのフラット化)にあります。コンパイラはコードを解析し、AST と Node VM コード評価の両方を用いて、スタイル付きコンポーネントを評価し直します。その結果、コードの共有率が高まり、開発時間が短縮され、より軽量で高速なアプリを実現しています。

公式サイトのベンチマークを見てもかなり速いことがわかります。

使ってみる

インストール

create-tamagui で Web は Next.js、ネイティブは React Native (Expo) を使用したモノレポのプロジェクトテンプレートを作成できます。

テンプレートでは solito というライブラリが使用されています。solito の内容は 2 つです。

  • React Navigation (React Native 用のルーティングライブラリ) と Next.js の小さなラッパーであり、異なるプラットフォーム間でナビゲーションのコード共有することが可能
  • React Native + Next.js でクロスプラットフォームアプリ開発を行う際パターンとサンプルの集まり

https://solito.dev/

npm install -g yarn
npm create tamagui@latest tamagui-test
cd tamagui-test

# web
yarn web
open http://localhost:3000

# native
yarn native

このような画面が表示されるはずです。ダークテーマかどうかは端末によって異なります。

Web

モバイル

内容

Welcome ページのコードを少し見てみます。

packages/app/features/home/screen.tsx
import { Anchor, Button, H1, Input, Paragraph, Separator, Sheet, XStack, YStack } from '@my/ui'
import { ChevronDown, ChevronUp } from '@tamagui/lucide-icons'
import React, { useState } from 'react'
import { useLink } from 'solito/link'

export function HomeScreen() {
  const linkProps = useLink({
    href: '/user/nate',
  })

  return (
    <YStack f={1} jc="center" ai="center" p="$4" space>
      <YStack space="$4" maw={600}>
        <H1 ta="center">Welcome to Tamagui.</H1>
        <Paragraph ta="center">
          Here's a basic starter to show navigating from one screen to another. This screen uses the
          same code on Next.js and React Native.
        </Paragraph>

        <Separator />
        <Paragraph ta="center">
          Made by{' '}
          <Anchor color="$color12" href="https://twitter.com/natebirdman" target="_blank">
            @natebirdman
          </Anchor>
          ,{' '}
          <Anchor
            color="$color12"
            href="https://github.com/tamagui/tamagui"
            target="_blank"
            rel="noreferrer"
          >
            give it a ⭐️
          </Anchor>
        </Paragraph>
      </YStack>

      <XStack>
        <Button {...linkProps}>Link to user</Button>
      </XStack>

      <SheetDemo />
    </YStack>
  )
}

...

YStack XStack コンポーネントは Tamagui が用意している Stack コンポーネントを styled で拡張したものです。

const variants = {
  fullscreen: {
    true: fullscreenStyle,
  },
  elevation: {
    '...size': getElevation,
  },
} as const;

export const YStack = styled(Stack, {
  flexDirection: 'column',
  name: 'YStack',
  variants,
});

export const XStack = styled(Stack, {
  flexDirection: 'row',
  name: 'XStack',
  variants,
});

YStack のプロパティにある見かけない fjc ai などですが、これは Shorthands を呼ばれる Tamagui の機能の 1 つです。プロパティ名の短縮であり、Tamagui が用意しているものもありますが、ユーザーが作成することもできます。fflexjcjustifyContentaialignItems です。

export declare const shorthands: {
    readonly ussel: "userSelect";
    readonly cur: "cursor";
    readonly pe: "pointerEvents";
    readonly col: "color";
    readonly ff: "fontFamily";
    readonly fos: "fontSize";
    readonly fost: "fontStyle";
    readonly fow: "fontWeight";
    readonly ls: "letterSpacing";
    readonly lh: "lineHeight";
    readonly ta: "textAlign";
    readonly tt: "textTransform";
    readonly ww: "wordWrap";
    ...
}

続いて YStack のプロパティにある p="$4" が気になります。p に関しては上記で説明した機能の Shorthands であり padding の短縮系です。$4 は Tamagui が用意しているユーティリティです。公式サイトの下記画像を確認すると p="$4"padding="18px" と同等であることがわかります。Tamagui ではトークンと呼ばれています。こちらのトークンも Tamagui が用意しているものもありますが、ユーザーが作成することもできます。

まとめ

開発工数やエンジニアが少ない状況で、React + React Native で開発をする場合、強力な味方になってくれそうなライブラリです。全体的に特に難しい要素はなく使いやすく感じました。Web と ネイティブの間でコンポーネントの使い回しやスタイルの共通化ができるため開発速度の高速化が期待できそうです。また、現在は Web のみ開発を行っているが、将来的に React Native の開発も行うといった場合にも有効でしょう。まだまだ紹介しきれていない機能が沢山あるので是非一度手元で動かしてみていただけると便利さを実感していただけると思います。

最後までお読みいただきありがとうございました🙇‍♂️

Discussion

「アッ。もう記事になってる。どれどれ(._.)ってワシがオススメしたのか^^;」
というわけで、お手間かけてしまいました。心ばかりのバッジ贈りました。

6年前のレガシーなReactプロダクトをメンテするばかりなのに、フロントエンドの選択肢は広がる一方で躊躇しますが、Reactのエコシステムには毎度頭が下がります。時間を見て試してみたいと思います。

読んでいただけるだけで大変ありがたいのですがバッチまで...
ありがとうございます🙇‍♂️
少しでもご参考になれば幸いです。

ログインするとコメントできます