LoginSignup
1
0

More than 1 year has passed since last update.

【React Native】Contextを利用して複数テーマを簡単にきりかえる

Posted at

はじめに

React Nativeのアプリ開発で、将来的なダークテーマの追加などに対応できるように、複数テーマを動的にきりかえることができるようなしくみを実装しました。

以下の記事と我が師匠のアドバイスをもとに、自分なりにカスタマイズしてみました。

実装

テーマの定義、ContextとProviderの作成、カスタムフックの作成などについて記載します。

テーマ(色)の定義

まず、ColorThemeに各カテゴリー(Textなど)に含まれる特徴(primaryなど)の型を定義します。
カテゴリーの分け方はアプリ次第ですが、TextやButton以外にBackgroundやDividerなども考えられるかと思います。

次に、~Emphasisとして、各カテゴリの特徴をリテラル型として定義します。
これは、例えばButtonのコンポーネントを作成する際、テーマを決めるProps(emphasis)を追加するときに必要な型となります。

最後に、テーマを定義します。
DEFAULT_THEMEを基本テーマとして、新しいテーマが必要であればDARK_THEMEなどを追加していきます。

src/styles/theme/ColorTheme.ts
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でグローバルに保持するようにします。

src/styles/themeProvider/ThemeContext.tsx
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は空オブジェクトを返すように工夫しています。

src/styles/themeProvider/useTheme.ts
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ディレクトリ内につくったものは、一応バレル化します。

src/styles/themeProvider/index.tsx
export { ThemeProvider, ThemeContext } from "./ThemeContext";
export { useTheme } from "./useTheme";

useThemeの使用例

以下のように、StyleSheetのオブジェクトを返す_stylesを引数にとって、返したstylesをコンポーネントのstyleに適用することができます。

Example.tsx
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>
  );
};
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0