Unistyles 3.0を使ったらスタイルの定義がよりスッキリ書けそうな話

# はじめに

こんにちは、menu株式会社フロントエンドエンジニアの内田です。
menuのモバイルアプリの多くはReact Nativeを使っています。

突然ですが、みなさんは[Unistyles](https://www.unistyl.es/v3/start/introduction)をご存知ですか？
2025年現在、3.0がベータ版として公開されています。
すごく便利だなと思ったので、今回はUnistylesについて紹介します。

# Unistylesってなに？

Unistylesは、React NativeのStyleSheetを拡張したライブラリです。JavaScriptにおけるTypeScriptのような関係と言えます。従来のStyleSheetの利点を継承しつつ、より強力で柔軟なスタイリングを提供します。まだベータ版ではありますが、Expoなどのフレームワークからも注目されており、React Nativeにおけるスタイリングの新たな選択肢として期待されています。

ドキュメントにも記載されている通り、Unistylesは特にテーマの適用やvariantによるスタイルの切り替えを必要とするUIコンポーネントに有効です。また、アプリ全体の再レンダリングを抑制し、パフォーマンス向上にも貢献します（New Architecture最適化）。

一方で、シンプルなアプリなど、高度なスタイリング機能が不要な場合には、Unistylesの恩恵は限定的かもしれません。また、現時点ではNew Architectureにのみ対応しており、Expo Goでは実行できないなどの制約もあります。


## Unistylesの基本的な使い方

### 従来のスタイリングの辛いところ

まず、従来のReact Nativeでのスタイリングの問題点を確認します。以下のコードは、variant、size、disabledといったpropsに基づいてスタイルを切り替えるボタンコンポーネントの例です。

<details><summary>ボタンコンポーネント実装例</summary>

```typescript
export const ContainedButton: React.FC<ContainedButtonProps> = ({
  label,
  variant = 'primary',
  size = 'medium',
  disabled = false,
  style,
  onPress,
}) => {
  const styles = useMemo(
    () =>
      StyleSheet.create({
        button: {
          alignItems: 'center',
          backgroundColor: disabled ? colors.button.disabled : colors.button[variant],
          borderRadius: 4,
          justifyContent: 'center',
          opacity: disabled ? 0.5 : 1,
          ...getSizeStyles(size),
        },
        text: {
          color: disabled
            ? colors.text.onDisabled
            : variant === 'inverse'
              ? colors.text.onInverse
              : colors.text.onPrimary,
          fontFamily: 'Poppins-Regular',
          fontSize: 16,
        },
      }),
    [variant, size, disabled]
  );

  return (
    <Pressable
      style={({ pressed }) => [
        styles.button,
        {
          opacity: pressed ? 0.8 : 1,
        },
        style as StyleProp<ViewStyle>,
      ]}
      onPress={onPress}
      disabled={disabled}
      accessibilityRole="button"
      accessibilityState={{ disabled }}
    >
      <Text style={styles.text}>{label}</Text>
    </Pressable>
  );
};

function getSizeStyles(size: ButtonSize) {
  switch (size) {
    case 'small':
      return {
        paddingVertical: 2,
        paddingHorizontal: 4,
      };
    case 'large':
      return {
        paddingVertical: 6,
        paddingHorizontal: 12,
      };
    case 'medium':
    default:
      return {
        paddingVertical: 4,
        paddingHorizontal: 8,
      };
  }
}
```

</details>

このコンポーネントでは、propsの値に応じてスタイルを切り替えるために,render処理内に条件分岐を記述する必要があり、コードが複雑になりがちです。また、これに加えてテーマなども注入するとなると、色々ごちゃごちゃしてきそうです。

### Unistylesによるスタイリング
ではUnistylesを使用するとどうなるのでしょう。ありがたいことに、Unistylesは、React Native提供のStyleSheetと同じシンタックスで記述できます。

variantsキーを使用することで、動的に変更するスタイルプロパティを宣言的に記述できます。これにより、スタイルの定義をコンポーネントの外部に分離でき、コードの見通しが良くなります。

<details><summary>スタイル実装例</summary>

```typescript
const styles = StyleSheet.create(() => ({
  button: {
    alignItems: 'center',
    borderRadius: 4,
    justifyContent: 'center',

    variants: {
      color: {
        primary: {
          backgroundColor: colors.button.primary,
        },
        secondary: {
          backgroundColor: colors.button.secondary,
        },
        tertiary: {
          backgroundColor: colors.button.tertiary,
        },
        inverse: {
          backgroundColor: colors.button.inverse,
        },
      },
      disabled: {
        true: {
          backgroundColor: colors.button.disabled,
          opacity: 0.5,
        },
        false: {},
      },
      size: {
        small: {
          paddingVertical: 2,
          paddingHorizontal: 4,
        },
        medium: {
          paddingVertical: 4,
          paddingHorizontal: 8,
        },
        large: {
          paddingVertical: 6,
          paddingHorizontal: 12,
        },
      },
    },
  },
  text: {
    fontFamily: 'Poppins-Regular',
    fontSize: 16,
    variants: {
      color: {
        primary: {
          color: colors.text.onPrimary,
        },
        secondary: {
          color: colors.text.onSecondary,
        },
        tertiary: {
          color: colors.text.onTertiary,
        },
        inverse: {
          color: colors.text.onInverse,
        },
      },
    },
  },
}));
```

</details>

そして、コンポーネント内では、useVariantsフックを使用して、適用するvariantを指定します。

<details><summary>ボタンコンポーネント with Unistyles 実装例</summary>

```typescript
export const ContainedButton: React.FC<ContainedButtonProps> = ({
  label,
  variant = 'primary',
  size = 'medium',
  disabled = false,
  style,
  onPress,
}) => {
  styles.useVariants({
    color: variant,
    disabled,
    size,
  });

  return (
    <Pressable
      style={({ pressed }) => [
        styles.button,
        {
          opacity: pressed ? 0.8 : 1,
        },
        style as StyleProp<ViewStyle>,
      ]}
      onPress={onPress}
      disabled={disabled}
      accessibilityRole="button"
      accessibilityState={{ disabled }}
    >
      <Text style={styles.text}>{label}</Text>
    </Pressable>
  );
};
```

</details>

コンポーネント関数からスタイルの処理を分離することができたのでスッキリしましたね。今回は行いませんでしたが、UnistylesのStyleSheetはテーマの注入にも対応しているので、ダークモード・ライトモードの切り替えができるアプリでも非常に重宝しそうです。

# まとめ
Unistylesは、React Nativeのスタイリングをより柔軟かつ効率的に行うための強力なライブラリです。variantによるスタイルの切り替えだけでなく、テーマ機能など、さまざまな便利な機能を提供します。今後のメジャーリリースに期待しましょう。

今回は、Unistylesの基本的な使い方として、variantによるスタイルの切り替えを紹介しました。Unistylesのより詳細な情報については、公式ドキュメントを参照してください。

## カジュアル面談
モバイルアプリ開発について興味がある・menuの開発チームに興味があるなど、カジュアルに情報交換をしたい方を積極的に募集中です。下記のリンクからお気軽にご連絡ください。
[<img src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3980570/e438b7f7-8d9a-480e-b784-af50fc04b129.png">](https://pitta.me/matches/saqTXhJeIfiV)

<hr>

### **▼採用情報**<br>
レアゾン・ホールディングスは、「世界一の企業へ」というビジョンを掲げ、「新しい"当たり前"を作り続ける」というミッションを推進しています。

現在、エンジニア採用を積極的に行っておりますので、ご興味をお持ちいただけましたら、ぜひ下記リンクからご応募ください。

https://recruit.reazon.jp/