はじめに
React Nativeのアプリ開発で、将来的なダークテーマの追加などに対応できるように、複数テーマを動的にきりかえることができるようなしくみを実装しました。
以下の記事と我が師匠のアドバイスをもとに、自分なりにカスタマイズしてみました。
実装
テーマの定義、ContextとProviderの作成、カスタムフックの作成などについて記載します。
テーマ(色)の定義
まず、ColorTheme
に各カテゴリー(Textなど)に含まれる特徴(primaryなど)の型を定義します。
カテゴリーの分け方はアプリ次第ですが、TextやButton以外にBackgroundやDividerなども考えられるかと思います。
次に、~Emphasis
として、各カテゴリの特徴をリテラル型として定義します。
これは、例えばButton
のコンポーネントを作成する際、テーマを決めるProps(emphasis)を追加するときに必要な型となります。
最後に、テーマを定義します。
DEFAULT_THEME
を基本テーマとして、新しいテーマが必要であればDARK_THEME
などを追加していきます。
import { ColorValue } from "react-native";
// 型定義
export type ColorTheme = {
Text: {
primary: ColorValue;
secondary: ColorValue;
inverse: ColorValue;
disabled: ColorValue;
alert: ColorValue;
};
Button: {
primary: ColorValue;
secondary: ColorValue;
inverse: ColorValue;
disabled: ColorValue;
pressed: ColorValue;
alert: ColorValue;
};
};
// プロパティ名のリテラル
export type TextEmphasis = keyof ColorTheme["Text"];
export type ButtonEmphasis = keyof ColorTheme["Button"];
// テーマ定義
export const DEFAULT_THEME: ColorTheme = {
Text: {
primary: "#000000",
secondary: "#808080",
inverse: "#FFFFFF",
disabled: "#C7C7C7",
alert: "#FF0000",
},
Button: {
primary: "#0000FF",
secondary: "#000000",
inverse: "#FFFFFF",
disabled: "#C7C7C7",
pressed: "rgba( 255, 255, 255, 0.5)",
alert: "#FF0000",
},
};
ThemeContextとThemeProviderの作成
テーマtheme
とテーマきりかえtoggleTheme
について、Context APIでグローバルに保持するようにします。
import React, { ReactNode, createContext, useMemo, useState } from "react";
import { ColorTheme, DEFAULT_THEME } from "src/styles/theme/ColorTheme";
export type ContextValue<T extends ColorTheme> = {
theme: T;
toggleTheme: (theme: T) => void;
};
const defaultValue = {
theme: DEFAULT_THEME,
toggleTheme: () => undefined,
};
export const ThemeContext = createContext<ContextValue<any>>(defaultValue);
type Props = {
initialTheme: ColorTheme;
children: ReactNode;
};
export const ThemeProvider = React.memo(({ initialTheme, children }: Props) => {
const [currentTheme, setCurrentTheme] = useState<ColorTheme>(initialTheme);
const contextValue = useMemo<ContextValue<any>>(() => {
return {
theme: currentTheme,
toggleTheme: theme => setCurrentTheme(theme),
};
}, [currentTheme]);
return <ThemeContext.Provider value={contextValue}>{children}</ThemeContext.Provider>;
});
ThemeProvider.displayName = "ThemeProvider";
作成したThemeProvider
はRootに設置します。
export const App = () => {
return (
<ThemeProvider initialTheme={DEFAULT_THEME}>
<Root />
</ThemeProvider>
);
};
カスタムフックuseThemeの作成
StyleSheetでContextのtheme
をつかえるように、カスタムフックを作成します。
引数なしの場合(stylesを使わずにthemeやtoggleThemeだけを使う場合)、stylesは空オブジェクトを返すように工夫しています。
import { useContext, useMemo } from "react";
import { StyleSheet } from "react-native";
import { ColorTheme, ThemeContext } from "src/styles/theme/ColorTheme";
import { ContextValue } from "src/styles/themeProvider/ThemeContext";
type Generator<T extends ColorTheme> = (theme: T) => ReturnType<typeof StyleSheet.create>;
export const useTheme = <T extends ColorTheme, U = {}>(fn?: Generator<T>) => {
const { theme, toggleTheme }: ContextValue<T> = useContext(ThemeContext);
const styles = useMemo<ReturnType<U>>(() => (fn ? fn(theme) : {}), [fn, theme]);
return { styles, theme, toggleTheme };
};
themeProviderディレクトリ内につくったものは、一応バレル化します。
export { ThemeProvider, ThemeContext } from "./ThemeContext";
export { useTheme } from "./useTheme";
useThemeの使用例
以下のように、StyleSheetのオブジェクトを返す_styles
を引数にとって、返したstyles
をコンポーネントのstyleに適用することができます。
export const Example = ({ children }: Props) => {
const { styles } = useTheme<ColorTheme, typeof _styles>(_styles);
return (
<View>
<Text style={styles.text}>{children}</Text>
</View>
);
};
const _styles = (theme: ColorTheme) => {
const styles = StyleSheet.create({
text: {
color: theme.Text.inverse,
},
});
return styles;
};
また、StyleSheetを使わない場合は、useThemeからtheme
を呼んで、直接コンポーネントのstyleに適用することもできます。
export const Example = ({ children }: Props) => {
const { theme } = useTheme();
return (
<View>
<Text style={{ color: theme.Text.inverse }}>{children}</Text>
</View>
);
};