|
| 1 | +--- |
| 2 | +id: themes |
| 3 | +title: Themes |
| 4 | +sidebar_label: Themes |
| 5 | +--- |
| 6 | + |
| 7 | +Providing a light theme and a dark theme is a nice way to let your users adjust the appearance of your app depending on the time of day or their preference. It also signals that you are a hip app developer that keeps up with the trends of the day. |
| 8 | + |
| 9 | +Building themes into an app with React Navigation is not too much different than a React app without it; the main differences are that you will need to use `screenProps` in order to update style properties controlled by `navigationOptions`, and when style properties are controlled in navigator configuration we'll need to take another approach. First we're going to recap how you would theme a React app without React Navigation, then we will dive deeper into these differences. Additionally, this guide assumes that you are already comfortable with React Navigation, in particular how to use and configure navigators. |
| 10 | + |
| 11 | +## Using React context to theme components |
| 12 | + |
| 13 | +React's context API allows you to share state from an ancenstor component to any of its descendents without explicitly passing the value through layers and layers of components ("prop drilling"). This is a useful tool in order to build themes because we can define the theme at the root of the app, and then access it from anywhere else and re-render every themed component whenever the theme changes. If you are not familiar with how to use context already, you might want to read the [React documentation](https://reactjs.org/docs/context.html) for it before continuing. |
| 14 | + |
| 15 | +```jsx |
| 16 | +import * as React from 'react'; |
| 17 | +import { Text, TouchableOpacity, View } from 'react-native'; |
| 18 | + |
| 19 | +const ThemeContext = React.createContext(null); |
| 20 | +const ThemeConstants = { |
| 21 | + light: { |
| 22 | + backgroundColor: '#fff', |
| 23 | + fontColor: '#000', |
| 24 | + }, |
| 25 | + dark: { |
| 26 | + backgroundColor: '#000', |
| 27 | + fontColor: '#fff', |
| 28 | + }, |
| 29 | +}; |
| 30 | + |
| 31 | +export default class AppContainer extends React.Component { |
| 32 | + state = { |
| 33 | + theme: 'light', |
| 34 | + }; |
| 35 | + |
| 36 | + toggleTheme = () => { |
| 37 | + this.setState(({ theme }) => ({ |
| 38 | + theme: theme === 'light' ? 'dark' : 'light', |
| 39 | + })); |
| 40 | + }; |
| 41 | + |
| 42 | + render() { |
| 43 | + return ( |
| 44 | + <ThemeContext.Provider |
| 45 | + value={{ theme: this.state.theme, toggleTheme: this.toggleTheme }}> |
| 46 | + <HomeScreen /> |
| 47 | + </ThemeContext.Provider> |
| 48 | + ); |
| 49 | + } |
| 50 | +} |
| 51 | + |
| 52 | +class HomeScreen extends React.Component { |
| 53 | + render() { |
| 54 | + return ( |
| 55 | + <ThemedView |
| 56 | + style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> |
| 57 | + <ThemeContext.Consumer> |
| 58 | + {({ toggleTheme }) => ( |
| 59 | + <ThemedButton title="Toggle theme" onPress={toggleTheme} /> |
| 60 | + )} |
| 61 | + </ThemeContext.Consumer> |
| 62 | + </ThemedView> |
| 63 | + ); |
| 64 | + } |
| 65 | +} |
| 66 | + |
| 67 | +class ThemedButton extends React.Component { |
| 68 | + render() { |
| 69 | + let { title, ...props } = this.props; |
| 70 | + return ( |
| 71 | + <TouchableOpacity {...props}> |
| 72 | + <ThemeContext.Consumer> |
| 73 | + {({ theme }) => ( |
| 74 | + <Text style={{ color: ThemeConstants[theme].fontColor }}> |
| 75 | + {title} |
| 76 | + </Text> |
| 77 | + )} |
| 78 | + </ThemeContext.Consumer> |
| 79 | + </TouchableOpacity> |
| 80 | + ); |
| 81 | + } |
| 82 | +} |
| 83 | + |
| 84 | +class ThemedView extends React.Component { |
| 85 | + render() { |
| 86 | + return ( |
| 87 | + <ThemeContext.Consumer> |
| 88 | + {({ theme }) => ( |
| 89 | + <View |
| 90 | + {...this.props} |
| 91 | + style={[ |
| 92 | + this.props.style, |
| 93 | + { backgroundColor: ThemeConstants[theme].backgroundColor }, |
| 94 | + ]} |
| 95 | + /> |
| 96 | + )} |
| 97 | + </ThemeContext.Consumer> |
| 98 | + ); |
| 99 | + } |
| 100 | +} |
| 101 | +``` |
| 102 | + |
| 103 | +Okay, that's a lot of code. There isn't much going on here aside from passing the theme around through context and then pulling it out of context when we need it inside of themed component. Themed components like `ThemedView` and `ThemedButton` are useful to help you avoid constantly repeating theme context related boilerplate. |
| 104 | + |
| 105 | +## Themes inside `navigationOptions` |
| 106 | + |
| 107 | +A regrettable limitation of the current implementation of `navigationOptions` is that we are unable to access React context for use in properties such as `headerStyle` and `headerTintColor`. We can and should use them in properties that access React components, for example in `headerRight` we could provide a component like `ThemedHeaderButton`. To apply the theme to other properties we need to use `screenProps`. |
| 108 | + |
| 109 | +```jsx |
| 110 | +import { createAppContainer, createStackNavigator } from 'react-navigation'; |
| 111 | + |
| 112 | +class HomeScreen extends React.Component { |
| 113 | + static navigationOptions = ({ screenProps }) => { |
| 114 | + let currentTheme = ThemeConstants[screenProps.theme]; |
| 115 | + |
| 116 | + return { |
| 117 | + title: 'Home', |
| 118 | + headerTintColor: currentTheme.fontColor, |
| 119 | + headerStyle: { backgroundColor: currentTheme.backgroundColor }, |
| 120 | + }; |
| 121 | + }; |
| 122 | + |
| 123 | + render() { |
| 124 | + return ( |
| 125 | + <ThemedView |
| 126 | + style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> |
| 127 | + <ThemeContext.Consumer> |
| 128 | + {({ toggleTheme }) => ( |
| 129 | + <ThemedButton title="Toggle theme" onPress={toggleTheme} /> |
| 130 | + )} |
| 131 | + </ThemeContext.Consumer> |
| 132 | + </ThemedView> |
| 133 | + ); |
| 134 | + } |
| 135 | +} |
| 136 | + |
| 137 | +const Stack = createStackNavigator({ Home: HomeScreen }); |
| 138 | +const Navigation = createAppContainer(Stack); |
| 139 | + |
| 140 | +export default class AppContainer extends React.Component { |
| 141 | + state = { |
| 142 | + theme: 'light', |
| 143 | + }; |
| 144 | + |
| 145 | + toggleTheme = () => { |
| 146 | + this.setState(({ theme }) => ({ |
| 147 | + theme: theme === 'light' ? 'dark' : 'light', |
| 148 | + })); |
| 149 | + }; |
| 150 | + |
| 151 | + render() { |
| 152 | + return ( |
| 153 | + <ThemeContext.Provider |
| 154 | + value={{ theme: this.state.theme, toggleTheme: this.toggleTheme }}> |
| 155 | + <Navigation screenProps={{ theme: this.state.theme }} /> |
| 156 | + </ThemeContext.Provider> |
| 157 | + ); |
| 158 | + } |
| 159 | +} |
| 160 | +``` |
| 161 | + |
| 162 | +Success! The stack header style now updates when the theme changes. |
| 163 | + |
| 164 | +> Note: in the future we would like to deprecate `screenProps` and move entirely over to React context. For now, `screenProps` is the best way to do that, and when this changes it will be easy to migrate. |
| 165 | +
|
| 166 | +## Theming tabs and other similar navigators |
| 167 | + |
| 168 | +Some navigators may have their styles configured in the navigator configuration object when they are initialized. While it may be best to update these navigators so that they can be configured more easily through `navigationOptions`, as long as they allow us to override the UI that they render with our own component and give us access to the default component, we can work with them just fine. We'll look at how to theme a bottom tab navigator. |
| 169 | + |
| 170 | +```jsx |
| 171 | +import { |
| 172 | + createAppContainer, |
| 173 | + createStackNavigator, |
| 174 | + createBottomTabNavigator, |
| 175 | + BottomTabBar, |
| 176 | +} from 'react-navigation'; |
| 177 | + |
| 178 | +const ThemeConstants = { |
| 179 | + light: { |
| 180 | + backgroundColor: '#fff', |
| 181 | + fontColor: '#000', |
| 182 | + activeTintColor: 'blue', |
| 183 | + inactiveTintColor: '#ccc', |
| 184 | + }, |
| 185 | + dark: { |
| 186 | + backgroundColor: '#000', |
| 187 | + fontColor: '#fff', |
| 188 | + activeTintColor: '#fff', |
| 189 | + inactiveTintColor: '#888', |
| 190 | + }, |
| 191 | +}; |
| 192 | + |
| 193 | +// Notice how we override the `activeTintColor`, `inactiveTintColor` and |
| 194 | +// `backgroundColor` of the tab bar with our theme styles. |
| 195 | +class ThemedBottomTabBar extends React.Component { |
| 196 | + render() { |
| 197 | + return ( |
| 198 | + <ThemeContext.Consumer> |
| 199 | + {({ theme }) => ( |
| 200 | + <BottomTabBar |
| 201 | + {...this.props} |
| 202 | + activeTintColor={ThemeConstants[theme].activeTintColor} |
| 203 | + inactiveTintColor={ThemeConstants[theme].inactiveTintColor} |
| 204 | + style={{ |
| 205 | + backgroundColor: ThemeConstants[theme].backgroundColor, |
| 206 | + }} |
| 207 | + /> |
| 208 | + )} |
| 209 | + </ThemeContext.Consumer> |
| 210 | + ); |
| 211 | + } |
| 212 | +} |
| 213 | + |
| 214 | +const Stack = createStackNavigator({ Home: HomeScreen }); |
| 215 | +const Tabs = createBottomTabNavigator( |
| 216 | + { Stack }, |
| 217 | + { tabBarComponent: ThemedBottomTabBar } |
| 218 | +); |
| 219 | +const Navigation = createAppContainer(Tabs); |
| 220 | + |
| 221 | +// And the rest of the code goes here... |
| 222 | +``` |
| 223 | + |
| 224 | +You will likely want to go a bit further than we detailed in this guide, such as change the status bar color depending on the theme and customize the border color for the header and tab bar as well. You can see all of the above code plus some more changes to make it more complete in [this Snack](https://snack.expo.io/@react-navigation/themes-example). |
| 225 | + |
| 226 | +I never said it was easy, but this about covers what you need to know to theme an app that uses React Navigation. Good luck, remember me you're a billionaire. |
0 commit comments