12.2 React Native Styling

# Review

# Simulated CSS

Similar to JavaScript styling for the web, React Native components use a style prop that takes an object of CSS like style properties. But this is not a browser. There is no real CSS support. React Native provides a familiar CSS like interface and translates that to native platform styling instructions.

# No Cascading

With the exception of nested <Text /> components, styling rules do not cascade down to their descendant components.

# The StyleSheet factory function

While you can use plain JavaScript objects for your styles, it is generally a better practice to use the StyleSheet.create() method to generate your style objects. It provides additional functionality to merge and manage styles in larger projects. By convention (not a technical requirement) styles are usually defined at the bottom of a component module.

TIP

The object passed into StyleSheet.create() can only be one level deep.

# Applying multiple styles to a component

In web development with CSS, you can apply multiple classes to inherit or combine styles. In React Native there are no classes, but you can achieve the same result by passing an array of style objects to the style prop of a component. e.g.

<Button title="Click Me" style={[styles.button, styles.buttonOutline]}>

# No units

All dimensions in React Native are unitless, and represent density-independent pixels. This applies to height, width, fontSize and any other style prop that affects the size of something.

# All views are flex containers

Nearly all layout positioning is done with flexbox styles. The View component and anything derived from it like ScrollView or FlatList are flex container. Their children can be positioned using standard CSS flexbox style directives like justifyContent: 'center'.

TIP

The default flexDirection in React Native is column, not row like in the browser.

A component must have a declared size or its children will not be visible. The most common declaration is flex: 1 to indicate that the container should take up all available space.

WARNING

A component can only expand to fill available space if its parent has dimensions greater than 0.

If a parent does not have explicitly defined width and height, or has a flex style prop set, that parent will have dimensions of 0 and the flex children will not be visible.

# Practice

Create an App component with three buttons placed in the center of the screen with a gap of 16dip between them. The colour should be indigo.

  • The first button should be a default text button.
  • The second button should have an outline border with rounded corners.
  • The third button should have a solid background colour with a pill shape.

Use the StyleSheet.create() method to define the styles for all of the components.

# Global Style System

As your application grows you will quickly encounter the need to create some consistent global style rules rather than repeating style code in every component. There are many possible solutions to this, but one of the best practices that I recommend is creating a top level styles folder and exporting simple JavaScript objects with your style system variables for spacing, colours, and typography, plus any global style rules that can then be composed in your components using StyleSheet.create(). This both improves consistency and readability.

Read more

John Schoeman and Vendela Larsson wrote a great article, React Native Styling: Structure for Style Organization explaining this technique.

# Example Code

As a reference example of this approach, this GitHub repo takes the movie list Summary/Detail view example from an earlier module and refactored it to use a global design system.

Spend a few minutes exploring this code and see the value in this approach to managing styles.

Credit

The colour palettes and spacing scales were borrowed from the "utility-first css framework" called tailwindcss.

# Supporting Dark Mode

It is becoming more and more common to have an app (or website) that supports both a light and dark theme. There is usually a setting or a UI switch that allows the user to change modes. It is also possible to have this selection set automatically based on the user's default OS selection.

In this exercise, you will learn how to make your components dynamically toggle between light mode and dark mode using React's Context API and React Native's useColorScheme hook.

# useColorScheme

The Appearance module gives you access to the user's preferred colour scheme (light, dark, null) as set on their device. This is supported on android 10+ and iOS 13+. However, this value may change periodically (e.g. auto switching at sunset and sunrise), so you will need to subscribe to changes.

Fortunately, the useColorScheme() hook encapsulates the various Appearance functions and provides a single subscribed value ready to consume.

Expo Configuration required

In order to activate support for appearance mode switching, you will need to update the app.json file with this setting

{
  "expo": {
    "userInterfaceStyle": "automatic"
  }
}

Without this setting, it will default to light to maintain backward compatibility.

See the docs for full details.

# Test it

Change the default App.js component in a new Expo managed React Native project to display the current user preference (if set).



 


 


 














import {StatusBar} from 'expo-status-bar'
import React from 'react'
import {StyleSheet, Text, View, useColorScheme} from 'react-native'

export default function App() {
  const colorScheme = useColorScheme()
  return (
    <View style={styles.container}>
      <Text>useColorScheme(): {colorScheme}</Text>
      <StatusBar style="auto" />
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center'
  }
})

# Clone Starter Code

Start by cloning the starter code from GitHub (this is the same repo from the section on global style systems above).

# Create theme objects

To make it easier to toggle between light mode and dark mode, it is helpful to create two theme objects that will have the same property names, but different color values. When creating the theme prop names, try to choose names that are descriptive, but generic enough to make sense in multiple places in the design. This is a starting point ...

/* /styles/themes.js */

import * as colors from './colors'

export const light = {
  primaryColor: colors.indigo800,
  accentColor: colors.indigo200,
  baseTextColor: colors.gray700,
  strongTextColor: colors.gray900,
  subduedTextColor: colors.gray500,
  inverseTextColor: colors.white,
  listSeparatorColor: colors.indigo100,
  bodyBackgroundColor: colors.indigo50
}

export const dark = {}

# Update components

Now that you have your theme objects, it is time to update the various UI components to use these properties rather than using the colors directly.

e.g.


 


 







 
 





/* App.js */
import {themes} from './styles'

function App() {
  const theme = themes.light

  return (
    <SafeAreaProvider>
      <MoviesProvider>
        <NavigationContainer>
          <Stack.Navigator
            screenOptions={{
              headerStyle: {backgroundColor: theme.primaryColor},
              headerTintColor: theme.inverseTextColor
            }}
          >
        >
  // ...      
/* MovieListScreen.js */

import {themes, spacing} from '../styles'
// ...

<FlatList style={styles.container}

// ... 
const theme = themes.light
const styles = StyleSheet.create({
  container: {
    padding: spacing.base,
    backgroundColor: theme.bodyBackgroundColor
  }
})

# Update the remaining components

OK. Now you should update the remaining two components to use the theme provided variables where appropriate.

  • MovieListItem.js
  • MovieDetailScreen.js

# Let's add some Context

That was a big step in the refactoring, but the app is still using the light theme that we started with. It is time to pass the theme to your components using a Context Provider and a custom hook.

Create a ThemeContext.js module in the /context folder. The ThemeProvider() method will follow the same pattern that we have used in previous examples and will provide the current theme name, the themes object (imported from styles) and a toggleTheme function. The default theme will be set to the user's platform default value with the useColorScheme hook.

/* /context/ThemeContext.js (part one)*/
import React from 'react'
import {useColorScheme} from 'react-native'
import {themes} from '../styles'

const ThemeContext = React.createContext()

function ThemeProvider(props) {
  // defaultTheme is either 'dark' or 'light'
  const defaultTheme = useColorScheme()
  const [themeName, setThemeName] = React.useState(defaultTheme)

  function toggleTheme() {
    setThemeName(currentTheme => (currentTheme === 'light' ? 'dark' : 'light'))
  }

  return (
    <ThemeContext.Provider
      value={{themeName, themes, toggleTheme}}
      {...props}
    />
  )
}
// ... rest of module

# The custom hook ...

You now want to create a custom useTheme hook. It too should follow the basic pattern that we have used before, but this time we need to add a little extra functionality. Our hook should take an optional function parameter that will inject the current theme into a StyleSheet object and return the resulting styles. For maximum flexibility in setting styles on components, the hook should return an object with the styles generated from the parameter function, the current theme name, the current theme object, and the toggleTheme function.

/* /context/ThemeContext.js (part two)*/

function useTheme(stylesheetBuilder) {
  const context = React.useContext(ThemeContext)
  if (!context)
    throw new Error('useTheme hook must be called within a ThemeProvider')

  const {themeName, themes, toggleTheme} = context
  const theme = themes[themeName]

  let styles = {}
  if (stylesheetBuilder && typeof stylesheetBuilder === 'function')
    styles = stylesheetBuilder(theme)

  return {styles, themeName, theme, toggleTheme}
}

export {ThemeProvider, useTheme}

# Add the theme to your components

Update the main App.js component to import the ThemeProvider and wrap it around the existing JSX. Because we would actually like to use the theme in the App component, create a simple ThemeWrapper component that will be the default export from App.js. The updated module should look like this ...









 



 
 
 
 
 
 
 


 







 
 






















 

import React from 'react'
import {NavigationContainer} from '@react-navigation/native'
import {createStackNavigator} from '@react-navigation/stack'
import {SafeAreaProvider} from 'react-native-safe-area-context'
import {MoviesProvider} from './context/moviesContext'
import MovieListScreen from './components/MovieListScreen'
import MovieDetailsScreen from './components/MovieDetailScreen'
import {StatusBar, View} from 'react-native'
import {ThemeProvider, useTheme} from './context/themeContext'

const Stack = createStackNavigator()

function ThemeWrapper() {
  return (
    <ThemeProvider>
      <App />
    </ThemeProvider>
  )
}

function App() {
  const {theme} = useTheme()

  return (
    <SafeAreaProvider>
      <MoviesProvider>
        <NavigationContainer>
          <Stack.Navigator
            screenOptions={{
              headerStyle: {backgroundColor: theme.primaryColor},
              headerTintColor: theme.inverseTextColor
            }}
          >
            <Stack.Screen
              name="MovieList"
              component={MovieListScreen}
              options={{
                title: 'Movie List'
              }}
            />
            <Stack.Screen
              name="MovieDetails"
              component={MovieDetailsScreen}
              options={{title: 'Movie Details'}}
            />
          </Stack.Navigator>
        </NavigationContainer>
        <StatusBar barStyle="light-content" />
      </MoviesProvider>
    </SafeAreaProvider>
  )
}

export default ThemeWrapper

WARNING

When the device setting changes to dark mode the default status bar colour also reverses.

# MovieListScreen

In the compositional components, you can now access the correct theme and incorporate it into the styles local to that component. First import the custom hook and invoke it with a theme constructor function (we will look at that in a second).


 



 



// other imports
import {useTheme} from '../context/themeContext'

function MovieListScreen({navigation}) {
  const [movies] = useMovies()
  const {styles} = useTheme(styleSheet)

// ... rest of module

# The styleSheet function

Convert the previous styles assignment statement to be a function that takes the current theme as an argument. This will get passed into the useTheme hook and generate the styles object that you can use in your JSX.

const styleSheet = theme =>
  StyleSheet.create({
    safeArea: {
      flex: 1,
      backgroundColor: theme.bodyBackgroundColor
    },
    container: {
      padding: spacing.base,
      backgroundColor: theme.bodyBackgroundColor
    }
  })

# Practice

Update the other two components using this example.

# Composing common styles

Often there are a combination of styles that you would like to apply across the application and in browser land you would create a global CSS class for that. You can do this with the global style objects that we created earlier. e.g.

/* /styles/typography.js */
export const bodyText = {
  color: colors.baseText,
  fontSize: baseFontSize,
  fontWeight: buttonFontWeight,
  lineHeight: baseLineHeight
}

But what happens when you want to modify some of those properties based on the selected theme? The React Native solution to this is to use JavaScript's assignment destructuring to mix properties. e.g.

/* /styles/themes.js */
export const light = {
  // ...
  
  bodyText: {
    ...typography.bodyText,
    color: colors.gray200
  }
}

# Add the dark theme object

Time to add the dark theme object properties. The final themes.js module should look like this ...

import * as colors from './colors'
import * as typography from './typography'

export const light = {
  primaryColor: colors.indigo800,
  accentColor: colors.indigo200,
  baseTextColor: colors.gray700,
  highlightTextColor: colors.indigo800,
  strongTextColor: colors.gray900,
  subduedTextColor: colors.gray500,
  inverseTextColor: colors.white,
  listSeparatorColor: colors.indigo100,
  bodyBackgroundColor: colors.indigo50,
  bodyText: {
    ...typography.bodyText,
    color: colors.gray700
  }
}
export const dark = {
  primaryColor: colors.gray800,
  accentColor: colors.indigo800,
  baseTextColor: colors.gray200,
  highlightTextColor: colors.indigo300,
  strongTextColor: colors.gray50,
  subduedTextColor: colors.gray300,
  inverseTextColor: colors.indigo300,
  listSeparatorColor: colors.gray600,
  bodyBackgroundColor: colors.gray700,
  bodyText: {
    ...typography.bodyText,
    color: colors.gray200
  }
}

# Test it out

Switch the active theme preference on your phone and refresh the Expo Client app.

Try adding a theme toggle button to the UI.

Last Updated: : 12/11/2020, 1:23:38 PM