はじめに
React Nativeでダークモード対応やレスポンシブ対応を実装しようとして、useColorSchemeやuseWindowDimensionsをあちこちのコンポーネントに埋め込み、画面サイズやテーマが変わるたびにコンポーネント全体が再レンダリングされて困った経験はありませんか。スタイルを変えるだけのはずなのに、パフォーマンスまで気にしなければならないのは本末転倒です。
この課題に、根本的なアプローチで応えるのがReact Native Unistylesです。スタイルの更新をReactの再レンダリングから切り離し、C++層で直接ネイティブビューを書き換えることで、驚くほど軽量なテーマ切り替えとレスポンシブ対応を実現します。今回はその仕組みと使い方を、コード例とともに紹介します。
React Native Unistylesとは
React Native Unistylesは、React Native標準のStyleSheetを置き換える形で使える、CSS-in-JSスタイリングライブラリです。「Level up your React Native StyleSheet」を掲げ、既存のStyleSheet.createとほぼ同じ書き方をしながら、テーマ・ブレークポイント・バリアントといった機能を追加できます。
最新のv3ではNitro Modulesを採用し、C++とJSIバインディングによる共有コアを持つアーキテクチャに刷新されました。これによりFabricのShadow Treeと密に統合され、スタイルの変更をReactのレンダーサイクルを介さずに反映できるようになっています。
主な特徴
- 再レンダリングが発生しない - テーマやブレークポイントが変わっても、影響を受けるコンポーネントのスタイルだけがネイティブ側で直接更新されます
- テーマとブレークポイントを標準サポート - ライト/ダークテーマの切り替えや、画面幅に応じたレスポンシブスタイルを追加のライブラリなしで書けます
- クロスプラットフォーム - iOS・Android・Webに加え、モノレポ構成でスタイルをほぼそのまま共有できます
- 既存コードからの移行が容易 -
StyleSheet.createの書き方をほぼ踏襲しているため、置き換えの学習コストが低く済みます
インストール
React Native Unistyles v3は、内部でNitro Modulesを利用するため、react-native-nitro-modulesも併せてインストールします。
# npm
npm install react-native-unistyles react-native-nitro-modules
# yarn
yarn add react-native-unistyles react-native-nitro-modules
# pnpm
pnpm add react-native-unistyles react-native-nitro-modules
v3の利用にはNew Architectureが有効なReact Native 0.78以上が必要です。また、スタイルの静的解析のためにBabelプラグインの設定が必須になります。babel.config.jsに以下を追加してください。
module.exports = function (api) {
api.cache(true)
return {
plugins: [
['react-native-unistyles/plugin', {
root: 'src', // アプリのソースコードのルートフォルダ
}],
],
}
}
基本的な使い方
テーマとブレークポイントの設定
まずはアプリ全体で使うテーマとブレークポイントを定義し、StyleSheet.configureで登録します。エントリーポイント(App.tsxなど)で一度だけ実行しておけばOKです。
// unistyles.ts
import { StyleSheet } from 'react-native-unistyles'
const lightTheme = {
colors: {
background: '#ffffff',
text: '#1a1a1a',
primary: '#5b8def',
},
}
const darkTheme = {
colors: {
background: '#121212',
text: '#f5f5f5',
primary: '#7ba7ff',
},
}
const appThemes = {
light: lightTheme,
dark: darkTheme,
}
const breakpoints = {
xs: 0,
sm: 400,
md: 700,
lg: 1024,
}
StyleSheet.configure({
themes: appThemes,
breakpoints,
settings: {
initialTheme: 'light',
},
})
type AppThemes = typeof appThemes
type AppBreakpoints = typeof breakpoints
declare module 'react-native-unistyles' {
export interface UnistylesThemes extends AppThemes {}
export interface UnistylesBreakpoints extends AppBreakpoints {}
}
コンポーネントでの利用
スタイル定義はStyleSheet.createに関数を渡すことで、テーマとブレークポイント情報(rt=runtime)を受け取れます。
import { View, Text, Pressable } from 'react-native'
import { StyleSheet } from 'react-native-unistyles'
const Card = ({ title }: { title: string }) => {
return (
<View style={styles.card}>
<Text style={styles.title}>{title}</Text>
<Pressable style={styles.button}>
<Text style={styles.buttonText}>詳細を見る</Text>
</Pressable>
</View>
)
}
const styles = StyleSheet.create((theme, rt) => ({
card: {
backgroundColor: theme.colors.background,
borderRadius: 12,
padding: 16,
// ブレークポイントごとに値を切り替えられる
width: {
xs: '100%',
md: 320,
},
},
title: {
color: theme.colors.text,
fontSize: 18,
fontWeight: '600',
marginBottom: 8,
},
button: {
backgroundColor: theme.colors.primary,
paddingVertical: 10,
borderRadius: 8,
// Safe Area も rt から直接参照できる
marginBottom: rt.insets.bottom,
},
buttonText: {
color: '#ffffff',
textAlign: 'center',
fontWeight: '500',
},
}))
export default Card
ポイントは、テーマが切り替わってもCardコンポーネント自体は一切再レンダリングされないことです。Babelプラグインが静的解析したstylesへの依存関係をもとに、変更が必要なスタイルだけがネイティブ側で更新されます。
実践的なユースケース
ダークモードの切り替え
useUnistylesフックからテーマ操作用の関数を取得し、ボタン一つでライト/ダークを切り替えられます。
import { View, Pressable, Text } from 'react-native'
import { StyleSheet, useUnistyles } from 'react-native-unistyles'
const ThemeToggle = () => {
const { theme, rt } = useUnistyles()
const toggleTheme = () => {
rt.setTheme(rt.themeName === 'light' ? 'dark' : 'light')
}
return (
<View style={styles.container}>
<Pressable style={styles.toggle} onPress={toggleTheme}>
<Text style={styles.label}>
{rt.themeName === 'light' ? 'ダークモードへ' : 'ライトモードへ'}
</Text>
</Pressable>
</View>
)
}
const styles = StyleSheet.create((theme) => ({
container: {
padding: 16,
backgroundColor: theme.colors.background,
},
toggle: {
backgroundColor: theme.colors.primary,
padding: 12,
borderRadius: 8,
},
label: {
color: '#ffffff',
textAlign: 'center',
},
}))
export default ThemeToggle
バリアントによる見た目の出し分け
ボタンの種類ごとにスタイルを切り替えたいとき、条件分岐でスタイルを組み立てるのではなく、variantsを使うとすっきり書けます。
import { Pressable, Text } from 'react-native'
import { StyleSheet } from 'react-native-unistyles'
type ButtonProps = {
label: string
variant?: 'primary' | 'secondary' | 'danger'
size?: 'small' | 'large'
}
const Button = ({ label, variant = 'primary', size = 'small' }: ButtonProps) => {
styles.useVariants({ variant, size })
return (
<Pressable style={styles.button}>
<Text style={styles.text}>{label}</Text>
</Pressable>
)
}
const styles = StyleSheet.create((theme) => ({
button: {
borderRadius: 8,
alignItems: 'center',
variants: {
variant: {
primary: { backgroundColor: theme.colors.primary },
secondary: { backgroundColor: '#e5e5e5' },
danger: { backgroundColor: '#e5484d' },
},
size: {
small: { paddingVertical: 8, paddingHorizontal: 16 },
large: { paddingVertical: 14, paddingHorizontal: 24 },
},
},
},
text: {
fontWeight: '600',
variants: {
variant: {
primary: { color: '#ffffff' },
secondary: { color: '#1a1a1a' },
danger: { color: '#ffffff' },
},
},
},
}))
export default Button
タブレット対応が必要な画面では、先ほどのブレークポイントと組み合わせることで、widthやflexDirectionをデバイスサイズごとに切り替えるレスポンシブレイアウトも簡単に組めます。
まとめ
React Native Unistylesは、単なる「見た目を整えるためのCSS-in-JS」にとどまらず、テーマ切り替えやレスポンシブ対応で発生しがちな再レンダリングの問題そのものを解決するライブラリです。
StyleSheet.createとほぼ同じ書き方で導入でき、学習コストが低い- テーマ・ブレークポイント・バリアントが標準機能として揃っている
- v3ではNitro ModulesとFabricの統合により、スタイル更新がReactのレンダーサイクルを経由しない
ダークモード対応やマルチデバイス対応を今後の実装で予定しているなら、標準のStyleSheetから置き換える価値は十分にあるはずです。まずは公式ドキュメント(unistyl.es)のGetting Startedに沿って、小さなコンポーネントから試してみてください。