GabeCunningham
GabeCunningham

Reputation: 103

React Native Context for changing themes

I have an app with around 6 screens, and wanted to write a consistent theme manager for them. I feel like I am most of the way there, but I'm struggling with the last little bit. How I want to have it is that there is a ThemesScreen containing a list of themes, and clicking on each one changes it dynamically, and I can see that clicking on the button does change the name, but I'm struggling with getting that to pass along. Here's my ThemeManager.js:

import React, { createContext, useState } from "react"; 

import { current } from "../Styles/themes";

export const ThemeContext = React.createContext();

export const ThemeProvider = ({ children }) =>{
    //light, dark
    const[theme, setTheme] = React.useState("light");

    const toggleTheme = () =>{
        if(theme === 'light'){
            setTheme("dark");

        }
        else{
            setTheme("light");
        }
        console.log(theme)
    }

    return (
        <ThemeContext.Provider value={{ theme, toggleTheme }}>
          {children}
        </ThemeContext.Provider>
    )
}

Here's my ThemesScreen.js:

import React, { useContext } from 'react';
import { Text, ScrollView, StyleSheet, Alert } from 'react-native';

import { RowItem, RowSeparator } from '../Styles/RowItem';

import ThemeProvider, { ThemeContext } from '../utils/ThemeManager';

function ThemeScreen(){ 
    const theme = useContext(ThemeContext)
    return (
        <ThemeContext.Consumer>
            <ScrollView style={styles.row}>
                <RowItem
                    title="Light theme"
                    onPress={toggleTheme}/>
                <RowSeparator />
                <RowItem 
                    title="Dark Theme"
                    onPress={() => alert('Dark!')}/>
                <RowSeparator />
                <RowItem
                    title="Material Blue"
                    onPress={() => alert('Material blue!')}/>
            </ScrollView>   
        </ThemeContext.Consumer>
    )
}

const styles = StyleSheet.create({
    row: {
        backgroundColor: theme.background,
      },
});

export default ThemeScreen

And here's my Themes.js

export const LightTheme = {
    dark: false,
    colors: {
       primary: 'rgb(255, 45, 85)',
       background: 'f5f5f5',
       card: 'rgb(255, 255, 255)',
       text: '#333333',
       border: 'rgba(175, 47, 47, .75)',
       notification: 'rgba(175, 47, 47, .35)',
    }
 }
 
 export const DarkTheme = {
    dark: true,
    colors: {
       primary: 'rgb(255, 45, 85)',
       background: 'rgb(0, 0, 0)',
       card: 'rgb(255, 255, 255)',
       text: 'rgb(28, 28, 30)',
       border: 'rgb(199, 199, 204)',
       notification: 'rgb(255, 69, 58)',
    },
 };

 const current = LightTheme

 export { current }

My original idea is that I can just set current whatever the name of the currently selected theme is, and then do something like the below code, since I have a lot of different screens that would save me time.

import { current } from './styles/themes.js;
...
const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: current.background,
        paddingTop: 25,
    },

But I'm not quite sure how to do that, or if that will work. I'd appreciate any and all advice! Also if there's a much better way to do this, please tell me.

Upvotes: 1

Views: 938

Answers (1)

Dimitri Kopriwa
Dimitri Kopriwa

Reputation: 14375

You can move your styles into the body of your components:


import { RowItem, RowSeparator } from '../Styles/RowItem';

import ThemeProvider, { ThemeContext } from '../utils/ThemeManager';

function ThemeScreen(){ 
    const theme = useContext(ThemeContext)

    const styles = StyleSheet.create({
      row: {
        backgroundColor: theme.background,
        },
    });
    return (
        <ThemeContext.Consumer>
            <ScrollView style={styles.row}>
                <RowItem
                    title="Light theme"
                    onPress={toggleTheme}/>
                <RowSeparator />
                <RowItem 
                    title="Dark Theme"
                    onPress={() => alert('Dark!')}/>
                <RowSeparator />
                <RowItem
                    title="Material Blue"
                    onPress={() => alert('Material blue!')}/>
            </ScrollView>   
        </ThemeContext.Consumer>
    )
}


export default ThemeScreen

I would also suggest to memoize your toggler and context to improve performance.

For theming your react-native application, have you considered using styled-components ?

We use it in our cross platform projects and add all you need to create themized application.

It will also split the UX logic out of your components, right now you need to manually import the theme from the context, and you have no API for a styled-component like the one you expect.

yarn add styled-components

Create a styled component:

const Container = styled.View(({ theme }) => ({
  backgroundColor: theme.colors.white,
}))

Add ./theme.js:

export const theme = {
  colors: {
    white: '#ffffff',
  }
}

And use the ThemeProvider from styled-components/native your App.tsx:

+  import { ThemeProvider } from 'styled-components/native'
+  import { theme } from './theme'
-  <App />
+  <ThemeProvider theme={theme}><App /></ThemeProvider>

It will still be possible to access the theme from within your component using useTheme() hook, but I advise you to try to keep the stylesheet logic separated at maximum.

Upvotes: 1

Related Questions