React Native custom theme selector
Tags: react, reactnative, typescript, tutorial
Published: 2021-11-01 18:15:05

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](https://gist.github.com/herbievine/01de968fea129c828ffc6e504b6e58d9)])

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](https://www.npmjs.com/package/@react-native-async-storage/async-storage) package to manage local storage in React Native.

The functions will look something like this:

```typescript
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	   
}
```

I saved this file [here](https://gist.github.com/herbievine/f92cf4cb2161bc9aaa842af41dca6092): `./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](https://beta.reactjs.org/learn/passing-data-deeply-with-context). 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:

```typescript
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>
  )
}
```

## 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:

```typescript
// 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,
  }
}
```

## 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:

```typescript
const App: React.FC = () => {
  const { theme } = useTheme()
    
  return (
    <View style={{ background: theme.background }}>
      ...
    </View>
  )
}
```

An example of mutating the colors:

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

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

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 :)