DEV Community

Cover image for React Native custom theme selector
Herbie
Herbie

Posted on

React Native custom theme selector

Theming a mobile app can be a tricky thing to do, and quite daunting if you're new to the react native and javascript ecosystem. But I've tried to make this post clear and straightforward, so you shouldn't have any issues (and if you do, leave them in the comments).

Step 1 - Defining your colors

Create a file and add all of your colors to it (I added it to ./src/lib/constants.ts [see a live example here])

You don't have to stick with light and dark, you can add custom themes such as sepia or navy.

Step 2 - Create functions to communicate with the native storage API

You need to create two functions to communicate with the native storage provider. This serves two purposes

  • It persists the theme on the local device
  • Allows local storage access for web, iOS and Android

You will need to this package to manage local storage in React Native.

The functions will look something like this:

const os = Platform.OS   
const webStorage = window.localStorage    
const appStorage = AsyncStorage  

const getItem = async (key: string) => {     
  if (key) {
    return os === 'web'
      ? webStorage.getItem(key)
      : await appStorage.getItem(key)
  }  

  return null      
}    



const setItem = async (key: string, payload: string) => {
  if (key && payload) {
    return os === 'web'
      ? webStorage.setItem(key, payload)
      : await appStorage.setItem(key, payload)
  }      

  return null      
}
Enter fullscreen mode Exit fullscreen mode

I saved this file here: ./src/lib/storage.ts

Step 3 - Creating a theme context

Due to the theme data being shared only with components, we can use React's Context API. This will provide a globally accessible state that you can use within all of your app. The context will hold two variables:

theme: 'light' | 'dark': you need this to know what theme is selected
setTheme: React.Dispatch<React.SetStateAction<'light' | 'dark'>>: this is to change the theme

The context will look something like this:

import { useColorScheme } from 'react-native'
import { getItem, setItem } from '../lib/storage'

export type ThemeOptions = 'light' | 'dark'

export interface ThemeContextInterface {
  theme: ThemeOptions
  setTheme: Dispatch<SetStateAction<ThemeOptions>>
}

export const ThemeContext = React.createContext<ThemeContextInterface | null>(
  null
)

const ThemeProvider: React.FC<{}> = ({ children }) => {
  // default theme to the system
  const scheme = useColorScheme()
  const [theme, setTheme] = useState<ThemeOptions>(scheme ?? 'dark')

  // fetch locally cached theme
  useEffect(() => {
    const fetchTheme = async () => {
      const localTheme = await getItem('theme')

      return localTheme
    }

    fetchTheme().then((localTheme) => {
      if (localTheme === 'dark' || localTheme === 'light') {
        setTheme(localTheme)
      }
    })
  }, [])

  // set new theme to local storage
  useEffect(() => {
    setItem('theme', theme)
  }, [theme])

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}
Enter fullscreen mode Exit fullscreen mode

Step 4 - Creating the hook

The hook is the middleman between the state and the UI. Its main purpose is to deliver the correct colors based on the Theme Context.

The useTheme hook looks like this:

// import ThemeContext and your colors

export interface Theme {
  background: string
  backgroundVariant: string
  text: string
  variant: string
  secondary: string
  secondaryVariant: string
  accent: string
  success: string
  warning: string
  error: string
}

const lightTheme: Theme = {
  background: LIGHT_THEME_BACKGROUND,
  backgroundVariant: LIGHT_THEME_BACKGROUND_VARIANT,
  text: LIGHT_THEME_TEXT,
  variant: LIGHT_THEME_VARIANT,
  secondary: LIGHT_THEME_SECONDARY,
  secondaryVariant: LIGHT_THEME_SECONDARY_VARIANT,
  accent: SEMERU_BRAND,
  success: SUCCESS,
  warning: WARNING,
  error: ERROR,
}

const darkTheme: Theme = {
  background: DARK_THEME_BACKGROUND,
  backgroundVariant: DARK_THEME_BACKGROUND_VARIANT,
  text: DARK_THEME_TEXT,
  variant: DARK_THEME_VARIANT,
  secondary: DARK_THEME_SECONDARY,
  secondaryVariant: DARK_THEME_SECONDARY_VARIANT,
  accent: SEMERU_BRAND,
  success: SUCCESS,
  warning: WARNING,
  error: ERROR,
}

interface UseThemeHook {
  theme: Theme
  setTheme: Dispatch<SetStateAction<'light' | 'dark'>>
}

const useTheme = (): UseThemeHook => {
  const { theme, setTheme } = useContext(ThemeContext)!

  if (theme === 'dark') {
    return {
      theme: darkTheme,
      setTheme,
    }
  }

  return {
    theme: lightTheme,
    setTheme,
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 5 - Enjoy!

All you need to do now is to use it in your UI. Import useTheme and use it as you please!

An example of consuming the colors:

const App: React.FC = () => {
  const { theme } = useTheme()

  return (
    <View style={{ background: theme.background }}>
      ...
    </View>
  )
}
Enter fullscreen mode Exit fullscreen mode

An example of mutating the colors:

const App: React.FC = () => {
  const { setTheme } = useTheme()

  return (
    <Pressable onPress={() => setTheme(prev => prev === 'light' ? 'dark' : 'light')}>
      <Text>Change theme</Text>
    </Pressable>
  )
}
Enter fullscreen mode Exit fullscreen mode

And that's it!

There is however a step 6, and that simply involves liking this post and sharing on Twitter. I would really appreciate it :)

Discussion (0)