Ollie
Ollie

Reputation: 243

Change theme in React via context

I have a context where I define my light and dark theme. Then I send the context to app where I defined a state that is passed to my Settings screen via my context provider value.

Here is my questions: If I want to toggle between dark and light via settings, where would I need to put my state? Or can I just define a function in App that I pass down via navigation somehow?

And my buttons that should toggle the theme are simple onPress buttons in the screen Settings.js, also my theme colors are already imported there via context obviously!

Please help!

App.js

import ThemeContext, {themes} from './contexts/ThemeContext';

const App = () => {
const [selectedTheme, setSelectedTheme] = useState(themes.light)


  return (
    <ThemeContext.Provider value={selectedTheme}>
            <Stack.Navigator>
              <Stack.Screen name="Settings" component={Settings}/>
            </Stack.Navigator>
    </ThemeContext.Provider>
  
  );
};

ThemeContext.js

import React from "react";
export const themes =  {
     
    dark: {
     
        background:{backgroundColor:"black"},
        text:{backgroundColor:"white"},
  

    },

    light:{
     
        background:{backgroundColor:"white"},
        text:{backgroundColor:"black"},
  

    },
}


const ThemeContext = React.createContext(themes.dark)
export default ThemeContext

Upvotes: 0

Views: 2318

Answers (2)

user23897462
user23897462

Reputation: 1

This repository has a good example of how to setup custom theming. https://github.com/florophore/floro-react-native-demo.

In your app.tsx you need something to manage the theme preference. Here is an example of such a provider.

ThemeSet in this example but would likely be a hashmap with keys light 'light' and 'dark'. The values would correspond to your predefined themes.

interface Props {
  children: React.ReactElement;
}

export const ThemePreferenceProvider = (props: Props): React.ReactElement => {
  const [themePreference, setThemePreference] = useState<
    'system' | keyof ThemeSet
  >('system');

  useEffect(() => {
    let closureIsFresh = true;
    AsyncStorage.getItem('theme-preference').then(
      (storedPreference: string | null) => {
        if (closureIsFresh) {
          setThemePreference(
            (storedPreference as keyof ThemeSet | 'system') ?? 'system',
          );
        }
      },
    );
    return () => {
      closureIsFresh = false;
    };
  }, []);

  const selectColorTheme = useCallback(
    (themePreference: 'system' | keyof ThemeSet) => {
      setThemePreference(themePreference);
      AsyncStorage.setItem('theme-preference', themePreference);
    },
    [],
  );

  const rnSystemColorScheme = useColorScheme();
  const currentTheme = useMemo(() => {
    if (themePreference == 'system') {
        if (!rnSystemColorScheme) {
            return (
              (
                Object.keys(initThemes.themeDefinitions) as Array<
                  keyof ThemeSet
                >
              )?.[0] ??
              (initThemes.themeDefinitions[
                defaultTheme
              ] as unknown as keyof ThemeSet)
            );
        }
        return initThemes.themeDefinitions[rnSystemColorScheme as keyof ThemeSet]?.name as keyof ThemeSet;
    }
    return themePreference as keyof ThemeSet;

  }, [themePreference, rnSystemColorScheme])

  return (
    <ThemePreferenceContext.Provider
      value={{
        themePreference,
        selectColorTheme,
        currentTheme
      }}>
      {props.children}
    </ThemePreferenceContext.Provider>
  );
};

export const useThemePreference = () => {
  return useContext(ThemePreferenceContext);
};

You then can define some pretty useful hooks. So you can use your themes in stylesheets and jsx.

interface InjectedStyles {
  palette: Palette;
  paletteColor: <K extends keyof Palette, S extends keyof Palette[K]>(
    key: K,
    shade: S,
    defaultValue?: string,
  ) => ColorValue;
  colorTheme: ColorTheme;
  themeColor: <K extends keyof ThemeColors>(
    key: K,
    variantKey?: keyof ThemeColors[K]['variants'] | 'default',
    defaultValue?: string,
  ) => ColorValue;
  background: ColorValue;
  width: number;
  height: number;
}

export const createUseStyles = <
  T extends StyleSheet.NamedStyles<T> | StyleSheet.NamedStyles<any>,
>(
  stylesCallback: (injected: InjectedStyles) => T,
): () => StyleSheet.NamedStyles<T> | StyleSheet.NamedStyles<any> => {
  return () => {
    return useStyles(stylesCallback);
  }
};

const useStyles = <
  T extends StyleSheet.NamedStyles<T> | StyleSheet.NamedStyles<any>,
>(
  stylesCallback: (injected: InjectedStyles) => T,
) => {

  const { width, height } = useWindowDimensions();
  const palette = useFloroPalette();
  const colorTheme = useColorTheme();
  const paletteColor = useMemo(
    () => makePaletteColorCallback(palette),
    [palette],
  );
  const themeColor = useMemo(() => makeThemeCallback(colorTheme), [colorTheme]);
  const background = useThemeBackground();
  const injectedStyles = useMemo((): InjectedStyles => {
    return {
      palette,
      paletteColor,
      colorTheme,
      themeColor,
      background,
      width,
      height
    } as InjectedStyles;
  }, [palette, colorTheme, paletteColor, themeColor, width, height]);
  return useMemo(() => {
    return StyleSheet.create(stylesCallback(injectedStyles));
  }, [stylesCallback, injectedStyles]);
};

export const usePaletteColor = <K extends keyof Palette, S extends keyof Shade>(
  key: K,
  shade: S,
  defaultValue?: string,
) => {
  const palette = useFloroPalette();
  const paletteColor = useMemo(() => makePaletteColorCallback(palette), []);
  return useMemo(() => {
    return paletteColor(key, shade, defaultValue);
  }, [paletteColor, key, shade, defaultValue]);
};

export const useThemeColor = <K extends keyof ThemeColors>(
  key: K,
  variantKey: keyof ThemeColors[K]['variants'] | 'default' = 'default',
  defaultValue?: string,
) => {
  const colorTheme = useColorTheme();
  const themeColor = useMemo(() => makeThemeCallback(colorTheme), [colorTheme]);
  return useMemo(() => {
    return themeColor(key, variantKey as 'default', defaultValue);
  }, [themeColor, key, variantKey, defaultValue]);
};

Then you can do things like this

const useStyles = createUseStyles(({paletteColor, themeColor, background}) => ({
  container: {
    height: 72,
    width: '100%',
  },
  headContainer: {
    width: '100%',
    flexDirection: 'row',
    padding: 8,
    justifyContent: 'space-between',
  },
  leftContainer: {
    flexDirection: 'row',
    alignItems: 'center',
  },
  debugButton: {
    marginLeft: 12,
    // palette API
    color: paletteColor('blue', 'regular'),
    fontSize: 16,
    fontWeight: 'bold'
  },
  rightContainer: {
    flexDirection: 'row',
    alignItems: 'center',
  },
  themeSwitcherContainer: {
    alignItems: 'center',
    width: 72,
    height: 32,
    borderWidth: 1,
    borderRadius: 16,
    // themeApi
    borderColor: themeColor('primary-border-theme'),
  },
  ...
});

interface Props {
    onOpenLanguages: () => void;
    onOpenDebug: () => void;
}

const MyComponent = (props: Props) => {

    const styles = useStyles();

    /**
     * themes api
    */
    const {currentTheme, selectColorTheme} = useThemePreference()
    const themeBackground = useThemeBackground();

    /**
     * icons API
    */
    const RoundIcon = useIcon("front-page.floro-round");
    const MoonIcon = useIcon("front-page.moon");
    const SunIcon = useIcon("front-page.sun");
    const LanguageIcon = useIcon("front-page.language");
    const DropDownArrow = useIcon("front-page.drop-down-arrow");

    const ThemeIcon = useMemo(() => {
        if (currentTheme == 'dark') {
            return MoonIcon
        }
        return SunIcon;
    }, [currentTheme, MoonIcon, SunIcon]);

    const onToggleTheme = useCallback(() => {
        selectColorTheme(currentTheme == 'light' ? 'dark' : 'light');
    }, [currentTheme])

    /**
     * text API
    */
    // usePlainText returns a string (this cannot be debugged, since it's just ASCII)
    const debugFloroText = usePlainText(
      'header.debug_floro',
    );
    return (
        <View style={styles.container}>
            <View style={styles.headContainer}>
                <View style={styles.leftContainer}>
                    <RoundIcon height={56}/>
                    <TouchableOpacity style={{zIndex: 200}} onPress={props.onOpenDebug}>
                        <View style={styles.row}>
                        <Text style={styles.debugButton}>{debugFloroText}</Text>
                        </View>
                    </TouchableOpacity>
                </View>
                <View style={styles.rightContainer}>
                    <TouchableOpacity onPress={onToggleTheme}>
                        <View style={styles.themeSwitcherContainer}>
                            <View style={styles.themeInnerContainer}>
                                <Animated.View style={{
                                    ...styles.themeCircle,
                                    backgroundColor: themeBackground,
                                    transform: [{
                                        translateX: themeTranslateX
                                    }]
                                }}>
                                    <ThemeIcon width={14} height={14}/>
                                </Animated.View>
                            </View>
                        </View>
                    </TouchableOpacity>
                    <View style={styles.divider}/>
                    <TouchableOpacity onPress={props.onOpenLanguages}>
                        <View style={styles.languageSwitcherContainer}>
                            <LanguageIcon width={24} height={24}/>
                            <DropDownArrow width={24} height={24}/>
                        </View>
                    </TouchableOpacity>
                </View>
            </View>
        </View>
    )

}

You should look at the repository though. It will do a much better job explaining than I can here.

Upvotes: 0

dominikjosch
dominikjosch

Reputation: 117

Add "setSelectedTheme" to your Context-Provider.

<ThemeContext.Provider value={{selectedTheme, setSelectedTheme}}>

You can than import your ThemeContext in the Compontens you need it and use it this way:

const currentTheme = useContext(ThemeContext)

If you want to change the Theme, you can then use:

currentTheme.setSelectedTheme = "light" 

wherever you need it.

Upvotes: 1

Related Questions