Using React Context page

Learn how to share a common theme by creating a React Context.

Overview

In this section, you will:

  • Create a theme with shared values.
  • Understand the what, why, and how of React Context.
  • Create a design system to unify the application.

Objective 1: Create a theme with shared values

Now that we understand how to style our components, let’s style everything!

Styling components individually can quickly become a maintenance nightmare. For instance, if we use #007980 for most highlights but occasionally use #ca2f35, what happens if we decide to change #007980 to #00a5ad?

To manage this efficiently, we need a shared theme. A shared theme helps us:

  • Keep styles in sync.
  • Avoid repetition.
  • Use common language to describe colors and other styling parameters.

Before we get too far into our style system though, we need to talk about how we will share it. While we could put it in a module that we import any time we need it, this would make it difficult to make dynamic changes to our styles, like dark mode. Instead, we can use React Context.

What is React Context?

Context is a feature in React that allows you to share data between components without having to explicitly pass props through every level of the component tree. It’s particularly useful for passing down global data (such as themes, user authentication, or language preferences) to components deep in the tree.

How do you use React Context?

Context consists of three parts:

  1. Context: Think of the context as a box of things. The box needs to be available to all who want to use it.
  2. Provider: The provider puts things into the box. Whatever data it handles is only available to its children.
  3. Consumer: The consumer takes things out of the box. It can only access the providers above it in the component hierarchy.

Creating the Context

First we create a definition of what information will be stored, then we get the object that we’ll use to store it. The createContext function also accepts a default value, to be provided whenever we access the context without a provider. In this case, we’ve decided to not specify a default value.

import {
  createContext,
  useCallback,
  useContext,
  useMemo,
  useState,
} from "react"
import { Text } from "react-native"

interface User {
  username: string
  token: string
}

interface AuthContext {
  signIn: (username: string, password: string) => Promise<void>
  signOut: () => Promise<void>
  user?: User
}

const Context = createContext<AuthContext | undefined>(undefined)

const App: React.FC = () => {
  const [user, setUser] = useState<User>()

  const signIn = useCallback(async (username: string, password: string) => {
    setUser({ username, token: password })
  }, [])

  const signOut = useCallback(async () => {
    setUser(undefined)
  }, [])

  const value = useMemo(
    () => ({ signIn, signOut, user }),
    [signIn, signOut, user],
  )

  return (
    <Context.Provider value={value}>
      <User />
    </Context.Provider>
  )
}

const User: React.FC = () => {
  const context = useContext(Context)

  return (
    <Text>
      {context?.user ? `Welcome ${context.user.username}!` : "Please sign in."}
    </Text>
  )
}

export default App

In the code above, we create an AuthContext that will have signIn and signOut methods, as well as a user object.

Returning the Provider

The Provider is how we actually tell React to store our data. We can render the provider anywhere in our tree, and the data will be accessible anywhere inside. To keep things performant, we will usually want to memoize these values. [Don’t worry if useMemo looks odd, we’ll be covering this shortly.]

import {
  createContext,
  useCallback,
  useContext,
  useMemo,
  useState,
} from "react"
import { Text } from "react-native"

interface User {
  username: string
  token: string
}

interface AuthContext {
  signIn: (username: string, password: string) => Promise<void>
  signOut: () => Promise<void>
  user?: User
}

const Context = createContext<AuthContext | undefined>(undefined)

const App: React.FC = () => {
  const [user, setUser] = useState<User>()

  const signIn = useCallback(async (username: string, password: string) => {
    setUser({ username, token: password })
  }, [])

  const signOut = useCallback(async () => {
    setUser(undefined)
  }, [])

  const value = useMemo(
    () => ({ signIn, signOut, user }),
    [signIn, signOut, user],
  )

  return (
    <Context.Provider value={value}>
      <User />
    </Context.Provider>
  )
}

const User: React.FC = () => {
  const context = useContext(Context)

  return (
    <Text>
      {context?.user ? `Welcome ${context.user.username}!` : "Please sign in."}
    </Text>
  )
}

export default App

Consuming the Context

We can useContext to access the data from our closest Provider.

import {
  createContext,
  useCallback,
  useContext,
  useMemo,
  useState,
} from "react"
import { Text } from "react-native"

interface User {
  username: string
  token: string
}

interface AuthContext {
  signIn: (username: string, password: string) => Promise<void>
  signOut: () => Promise<void>
  user?: User
}

const Context = createContext<AuthContext | undefined>(undefined)

const App: React.FC = () => {
  const [user, setUser] = useState<User>()

  const signIn = useCallback(async (username: string, password: string) => {
    setUser({ username, token: password })
  }, [])

  const signOut = useCallback(async () => {
    setUser(undefined)
  }, [])

  const value = useMemo(
    () => ({ signIn, signOut, user }),
    [signIn, signOut, user],
  )

  return (
    <Context.Provider value={value}>
      <User />
    </Context.Provider>
  )
}

const User: React.FC = () => {
  const context = useContext(Context)

  return (
    <Text>
      {context?.user ? `Welcome ${context.user.username}!` : "Please sign in."}
    </Text>
  )
}

export default App

Future-proofing your React Context

So far, we’ve covered the bare minimum for getting started with React Context. However, this simple approach poses a few problems:

  • We can’t store private information in the context.
  • Every time the provider is used, the component must be careful to create the memoized (and potentially very complicated) value.
  • If the structure of the data changes, you will have to update the code every place you used the provide and every place you used the consumer.

To help keep things clean in the future, it is best practice to keep this React Context object contained within this file, to avoid exporting it, and instead to export custom wrappers around each piece.

Future-proofing the Provider

We can create a custom AuthProvider that hides all the complicated logic from other components. Sometimes a custom provider will even take props (such as an access token), though this one doesn’t require any.

import {
  createContext,
  useCallback,
  useContext,
  useMemo,
  useState,
} from "react"

interface User {
  username: string
  token: string
}

interface AuthContext {
  signIn: (username: string, password: string) => Promise<void>
  signOut: () => Promise<void>
  user?: User
}

const Context = createContext<AuthContext | undefined>(undefined)

const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
  children,
}) => {
  const [user, setUser] = useState<User>()

  const signIn = useCallback(async (username: string, password: string) => {
    setUser({ username, token: password })
  }, [])

  const signOut = useCallback(async () => {
    setUser(undefined)
  }, [])

  const value = useMemo(
    () => ({
      signIn,
      signOut,
      user,
    }),
    [signIn, signOut, user],
  )

  return <Context.Provider value={value}>{children}</Context.Provider>
}

export default AuthProvider

export function useUser() {
  const { user } = useContext(Context)

  return user
}

export function useAuthentication() {
  const { signIn, signOut } = useContext(Context)

  return { signIn, signOut }
}

Future-proofing the Consumer

Instead of calling useContext everywhere and needing to understand the structure of the context, we can provide custom Hooks that return the specific parts of the context that we need. If we change the context structure, all we have to do is update these hooks to return the same data as before and no other code has to change.

import {
  createContext,
  useCallback,
  useContext,
  useMemo,
  useState,
} from "react"

interface User {
  username: string
  token: string
}

interface AuthContext {
  signIn: (username: string, password: string) => Promise<void>
  signOut: () => Promise<void>
  user?: User
}

const Context = createContext<AuthContext | undefined>(undefined)

const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
  children,
}) => {
  const [user, setUser] = useState<User>()

  const signIn = useCallback(async (username: string, password: string) => {
    setUser({ username, token: password })
  }, [])

  const signOut = useCallback(async () => {
    setUser(undefined)
  }, [])

  const value = useMemo(
    () => ({
      signIn,
      signOut,
      user,
    }),
    [signIn, signOut, user],
  )

  return <Context.Provider value={value}>{children}</Context.Provider>
}

export default AuthProvider

export function useUser() {
  const { user } = useContext(Context)

  return user
}

export function useAuthentication() {
  const { signIn, signOut } = useContext(Context)

  return { signIn, signOut }
}

Setup 1

✏️ Create src/design/theme/theme.ts and update it to be:

import { TextStyle } from "react-native"

interface Palette {
  main: string
  soft: string
  strong: string
  contrast: string
}

export interface Theme {
  palette: {
    screen: Palette
    primary: Palette
    secondary: Palette
  }
  spacing: {
    none: 0
    xs: number
    s: number
    m: number
    l: number
    xl: number
  }
  typography: {
    title: TextStyle
    heading: TextStyle
    body: TextStyle
    label: TextStyle
  }
}

export type ThemeMargin =
  | keyof Theme["spacing"]
  | [keyof Theme["spacing"], keyof Theme["spacing"]]
export type ThemePadding =
  | keyof Theme["spacing"]
  | [keyof Theme["spacing"], keyof Theme["spacing"]]

const theme: Theme = {
  palette: {
    screen: {
      main: "#ffffff",
      soft: "#e0e0e0",
      strong: "#ffffff",
      contrast: "#222222",
    },
    primary: {
      main: "#007980",
      soft: "#00a5ad",
      strong: "#006166",
      contrast: "#ffffff",
    },
    secondary: {
      main: "#ca2f35",
      soft: "#d93237",
      strong: "#a3262b",
      contrast: "#ffffff",
    },
  },
  spacing: {
    none: 0,
    xs: 4,
    s: 8,
    m: 16,
    l: 24,
    xl: 40,
  },
  typography: {
    title: {
      fontSize: 21,
      fontWeight: "500",
    },
    heading: {
      fontSize: 24,
      fontWeight: "500",
    },
    body: {
      fontWeight: "normal",
    },
    label: {
      fontSize: 12,
      fontWeight: "bold",
    },
  },
}

export default theme

✏️ Create src/design/theme/ThemeProvider.tsx and update it to be:

import { createContext, useContext, useMemo } from "react"

import theme, { Theme } from "./theme"

interface ThemeContext {
  theme: Theme
}

// Exercise: Update `ThemeProvider` to provide data according to the provided `ThemeContext`.

const ThemeProvider: React.FC = () => {
  // Exercise: Implement the ThemeProvider.
}

export default ThemeProvider

export function useTheme(): Theme {
  // Exercise: Consume the Context.
}

✏️ Create src/design/theme/index.ts and update it to be:

export type * from "./theme"

export { default } from "./ThemeProvider"
export * from "./ThemeProvider"

✏️ Update src/App.tsx to be:

import { SafeAreaView } from "react-native"

import ThemeProvider from "./design/theme/ThemeProvider"
import StateList from "./screens/StateList"

const App: React.FC = () => {
  return (
    <SafeAreaView style={{ flex: 1 }}>
      <ThemeProvider>
        <StateList />
      </ThemeProvider>
    </SafeAreaView>
  )
}

export default App

✏️ Update src/screens/StateList/components/ListItem/ListItem.tsx to be:

import { StyleSheet, Text } from "react-native"

import { Theme, useTheme } from "../../../../design/theme"

export interface ListItemProps {
  name: string
}

const ListItem: React.FC<ListItemProps> = ({ name }) => {
  // Exercise: Update `ListItem` to use the values from our context.
  const styles = getStyles(theme)

  return <Text style={styles.text}>{name}</Text>
}

function getStyles(theme: Theme) {
  return StyleSheet.create({
    text: {
      fontSize: 21,
      color: "#ffffff",
      backgroundColor: "#007980",
      padding: 16,
      marginVertical: 8,
    },
  })
}

export default ListItem

Verify 1

✏️ Create src/design/theme/ThemeProvider.test.tsx and update it to be:

import { render, screen } from "@testing-library/react-native"
import { View, Text } from "react-native"

import ThemeProvider, { useTheme } from "./ThemeProvider"

describe("Design/Theme", () => {
  describe("ThemeProvider", () => {
    it("renders children", async () => {
      render(
        <ThemeProvider>
          <Text>Hello!</Text>
        </ThemeProvider>,
      )

      expect(screen.getByText(/Hello/)).toBeOnTheScreen()
    })
  })

  describe("ThemeContext", () => {
    const TestComponent: React.FC = () => {
      const theme = useTheme()

      return (
        <View>
          <Text>{theme.palette.primary.main}</Text>
        </View>
      )
    }

    it("exposes context properties", async () => {
      render(<TestComponent />)

      expect(screen.getByText(/#007980/)).toBeOnTheScreen()
    })
  })
})

Exercise 1

  • Update ThemeProvider to provide data according to the provided ThemeContext.
  • Update ListItem to use the values from our context.

Solution 1

If you’ve implemented the solution correctly, the tests will pass when you run npm run test!

Click to see the solution

✏️ Update src/design/theme/ThemeProvider.tsx to be:

import { createContext, useContext, useMemo } from "react"

import theme, { Theme } from "./theme"

interface ThemeContext {
  theme: Theme
}

const Context = createContext<ThemeContext>({ theme })

export interface ThemeProviderProps {
  children: React.ReactNode
}

const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
  const value = useMemo(() => ({ theme }), [])

  return <Context.Provider value={value}>{children}</Context.Provider>
}

export default ThemeProvider

export function useTheme(): Theme {
  const { theme } = useContext(Context)

  return theme
}

✏️ Update src/screens/StateList/components/ListItem/ListItem.tsx to be:

import { StyleSheet, Text } from "react-native"

import { Theme, useTheme } from "../../../../design/theme"

export interface ListItemProps {
  name: string
}

const ListItem: React.FC<ListItemProps> = ({ name }) => {
  const theme = useTheme()
  const styles = getStyles(theme)

  return <Text style={styles.text}>{name}</Text>
}

function getStyles(theme: Theme) {
  return StyleSheet.create({
    text: {
      fontSize: theme.typography.title.fontSize,
      color: theme.palette.primary.contrast,
      backgroundColor: theme.palette.primary.main,
      padding: theme.spacing.m,
      marginVertical: theme.spacing.s,
    },
  })
}

export default ListItem

Objective 2: Begin creating a design system to unify your application

If we continue down this path, our entire codebase will become littered with theme.typography.*, theme.palette.*, etc. What if we decide to change the color pattern of one of our components? We'd have to update every place we used that pattern. Instead, we’ll create a design system.

What is a design system?

A design system is a collection of reusable components, guidelines, and principles that together help ensure consistency, efficiency, and scalability in designing and building digital products. It serves as a single source of truth for design and development teams, providing them with a set of predefined rules and assets to create cohesive user experiences across different platforms and devices.

Organizing a design system

If you ask three designers and three developers how best to organize a design system, you’ll get 9 different answers…

Atomic Design Methodology

The most prolific organization method is Atomic Design. If you want to learn more about design systems, Atomic Design by Brad Frost is the perfect place to start.

But… let’s keep things simple

Atomic Design is a great system, but it can be overkill for many projects. For this project, we’re going to keep things much more simple: We’ll have the shared theme we created in the previous objective, and we’ll create a small collection of exported components.

Setup 2

✏️ Create src/design/Box/Box.tsx and update it to be:

import {
  ViewProps,
  StyleSheet,
  ScrollView,
  View as StaticView,
} from "react-native"

import { Theme, ThemeMargin, ThemePadding, useTheme } from "../theme"

export interface BoxProps extends ViewProps {
  scrollable?: boolean
  margin?: ThemeMargin
  padding?: ThemePadding
}

const Box: React.FC<BoxProps> = ({
  scrollable = false,
  margin,
  padding,
  style,
  children,
  ...props
}) => {
  const theme = useTheme()
  const styles = getStyles(theme, { margin, padding })

  const View = scrollable ? ScrollView : StaticView

  return (
    <View style={StyleSheet.compose(styles.container, style)} {...props}>
      {children}
    </View>
  )
}

export default Box

function getStyles(
  theme: Theme,
  {
    margin,
    padding,
  }: {
    margin?: ThemeMargin
    padding?: ThemePadding
  },
) {
  return StyleSheet.create({
    container: StyleSheet.flatten([
      {
        display: "flex",
      },
      typeof margin === "string" && { margin: theme.spacing[margin] },
      Array.isArray(margin) && {
        marginVertical: theme.spacing[margin[0]],
        marginHorizontal: theme.spacing[margin[1]],
      },
      typeof padding === "string" && { padding: theme.spacing[padding] },
      Array.isArray(padding) && {
        paddingVertical: theme.spacing[padding[0]],
        paddingHorizontal: theme.spacing[padding[1]],
      },
    ]),
  })
}

✏️ Create src/design/Box/index.ts and update it to be:

export { default } from "./Box"
export * from "./Box"

✏️ Create src/design/Typography/Typography.tsx and update it to be:

import { TextProps, Text, StyleSheet } from "react-native"

import { Theme, useTheme } from "../theme"

export interface TypographyProps extends TextProps {
  variant?: keyof Theme["typography"]
}

const Typography: React.FC<TypographyProps> = ({
  style,
  variant = "body",
  children,
  ...props
}) => {
  // Exercise: Finish the Typography component using the same patterns used on the 'Box' component.
}

export default Typography

function getStyles(theme: Theme, variant: keyof Theme["typography"]) {
  return StyleSheet.create({
    text: {
      ...theme.typography[variant],
      color: theme.palette.screen.contrast,
    },
  })
}

✏️ Create src/design/Typography/index.ts and update it to be:

export { default } from "./Typography"
export * from "./Typography"

✏️ Update src/screens/StateList/StateList.tsx to be:

import { ScrollView, Text, View } from "react-native"

import Box from "../../design/Box"
import Typography from "../../design/Typography"

import ListItem from "./components/ListItem"

const states = [
  {
    name: "Illinois",
    short: "IL",
  },
  {
    name: "Wisconsin",
    short: "WI",
  },
]

const StateList: React.FC = () => {
  return (
    <ScrollView>
      <View>
        <Text>Place My Order: Coming Soon!</Text>
      </View>
      <View>
        {states?.length ? (
          states.map((state) => (
            <ListItem key={state.short} name={state.name} />
          ))
        ) : (
          <Text>No states found</Text>
        )}
      </View>
    </ScrollView>
  )
}

export default StateList

Verify 2

✏️ Create src/design/Box/Box.test.tsx and update it to be:

import { render, screen } from "@testing-library/react-native"

import Typography from "../Typography"

import Box from "./Box"

describe("Design/Box", () => {
  it("renders", () => {
    render(
      <Box padding="s" margin="s">
        <Typography>Hello!</Typography>
      </Box>,
    )

    expect(screen.getByText(/Hello/)).toBeOnTheScreen()
  })
})

✏️ Create src/design/Typography/Typography.test.tsx and update it to be:

import { render, screen } from "@testing-library/react-native"

import Typography from "./Typography"

describe("Design/Typography", () => {
  it("renders", () => {
    render(<Typography variant="body">Hello!</Typography>)

    expect(screen.getByText(/Hello/)).toBeOnTheScreen()
  })
})

Exercise 2

We’ve provided a basic Box component for you; because of the flexibility of the margin, padding, etc, a component like this can get complicated very quickly.

  • Finish the Typography component using the same patterns.
  • Update StateList to use Box and Typography instead of View and Text.

Solution 2

If you’ve implemented the solution correctly, the tests will pass when you run npm run test!

Click to see the solution

✏️ Update src/design/Typography/Typography.tsx to be:

import { TextProps, Text, StyleSheet } from "react-native"

import { Theme, useTheme } from "../theme"

export interface TypographyProps extends TextProps {
  variant?: keyof Theme["typography"]
}

const Typography: React.FC<TypographyProps> = ({
  style,
  variant = "body",
  children,
  ...props
}) => {
  const theme = useTheme()
  const styles = getStyles(theme, variant)

  return (
    <Text style={StyleSheet.compose(styles.text, style)} {...props}>
      {children}
    </Text>
  )
}

export default Typography

function getStyles(theme: Theme, variant: keyof Theme["typography"]) {
  return StyleSheet.create({
    text: {
      ...theme.typography[variant],
      color: theme.palette.screen.contrast,
    },
  })
}

✏️ Update src/screens/StateList/StateList.tsx to be:

import { ScrollView } from "react-native"

import Box from "../../design/Box"
import Typography from "../../design/Typography"

import ListItem from "./components/ListItem"

const states = [
  {
    name: "Illinois",
    short: "IL",
  },
  {
    name: "Wisconsin",
    short: "WI",
  },
]

const StateList: React.FC = () => {
  return (
    <ScrollView>
      <Box padding="s">
        <Typography variant="heading">Place My Order: Coming Soon!</Typography>
      </Box>
      <Box>
        {states?.length ? (
          states.map((state) => (
            <ListItem key={state.short} name={state.name} />
          ))
        ) : (
          <Typography>No states found</Typography>
        )}
      </Box>
    </ScrollView>
  )
}

export default StateList

Objective 3: Expand your design system

Now that we have the basics of a design system, we can expand it however we need. There are some basic components that most design systems will need:

  1. Page/Screen/etc. A main view of the app, which will often have a prop like title.
  2. Form components like TextInput, Select, Button. Almost every app will need forms of some kind.
  3. Typography. The basic text component, to make it easier to standardize text size and color.
  4. Grid. Most apps will need to control placement of elements on the screen.
  5. Card. While this is a specific design pattern, it has become very ubiquitous lately.

Beyond these, you’ll find all kinds of UI components like Tabs, Modal, Icon, Navigation, Badge, Progress, etc.

Setup 3

✏️ Create src/design/Button/Button.tsx and update it to be:

import {
  PressableProps,
  TextStyle,
  StyleSheet,
  Pressable,
  Text,
} from "react-native"

import { Theme, useTheme } from "../theme"

type Variant = "primary" | "secondary" | "outline"

export interface ButtonProps extends PressableProps {
  variant?: Variant
  margin?: keyof Theme["spacing"]
  padding?: keyof Theme["spacing"]
  fontSize?: TextStyle["fontSize"]
  fontWeight?: TextStyle["fontWeight"]
  disabled?: boolean
  children: string
}

const Button: React.FC<ButtonProps> = ({
  variant = "primary",
  margin,
  padding,
  fontSize = 20,
  fontWeight = "400",
  disabled,
  children,
  ...props
}) => {
  const theme = useTheme()
  const styles = getStyles(theme, variant)

  return (
    <Pressable
      style={StyleSheet.compose(styles.pressable, {
        ...(margin ? { margin: theme.spacing[margin] } : {}),
        ...(padding ? { padding: theme.spacing[padding] } : {}),
        opacity: disabled ? 0.5 : 1,
      })}
      disabled={disabled}
      {...props}
    >
      <Text
        style={StyleSheet.compose(styles.text, {
          fontSize,
          fontWeight,
        })}
      >
        {children}
      </Text>
    </Pressable>
  )
}

export default Button

function getStyles(theme: Theme, variant: Variant) {
  if (variant === "primary") {
    return StyleSheet.create({
      pressable: {
        margin: theme.spacing.s,
        padding: theme.spacing.m,
        borderRadius: 5,
        backgroundColor: theme.palette.primary.main,
      },
      text: {
        fontSize: 21,
        color: theme.palette.primary.contrast,
      },
    })
  }

  if (variant === "secondary") {
    return StyleSheet.create({
      pressable: {
        margin: theme.spacing.s,
        padding: theme.spacing.m,
        borderRadius: 5,
        backgroundColor: theme.palette.secondary.main,
      },
      text: {
        fontSize: 21,
        color: theme.palette.secondary.contrast,
      },
    })
  }

  if (variant === "outline") {
    return StyleSheet.create({
      pressable: {
        margin: theme.spacing.s,
        padding: theme.spacing.m - 1,
        borderRadius: 5,
        borderWidth: 1,
        backgroundColor: theme.palette.screen.main,
        borderColor: theme.palette.screen.contrast,
      },
      text: {
        fontSize: 21,
        color: theme.palette.screen.contrast,
      },
    })
  }

  throw new Error(`Button: Unknown variant: ${variant}`)
}

✏️ Create src/design/Button/Button.test.tsx and update it to be:

import { fireEvent, render, screen } from "@testing-library/react-native"

import Button from "./Button"

describe("Design/Button", () => {
  it("renders", () => {
    const handleChangeMock = jest.fn()

    render(<Button onPress={handleChangeMock}>Hello!</Button>)

    expect(screen.getByText(/Hello/)).toBeOnTheScreen()
    fireEvent.press(screen.getByText(/Hello/i))
    expect(handleChangeMock).toHaveBeenCalled()
  })
})

✏️ Create src/design/Button/index.ts and update it to be:

export { default } from "./Button"
export * from "./Button"

✏️ Create src/design/Card/Card.tsx and update it to be:

import { StyleSheet } from "react-native"

import Box, { BoxProps } from "../Box"
import { Theme, useTheme } from "../theme"
import Typography from "../Typography"

export interface CardProps extends BoxProps {
  title?: string
}

const Card: React.FC<CardProps> = ({ title, children, ...props }) => {
  const theme = useTheme()
  const styles = getStyles(theme)

  return (
    <Box margin={["m", "none"]} style={styles.container} {...props}>
      {title && (
        <Box padding="m" style={styles.title}>
          <Typography variant="title">{title}</Typography>
        </Box>
      )}

      <Box padding="m">{children}</Box>
    </Box>
  )
}

export default Card

function getStyles(theme: Theme) {
  return StyleSheet.create({
    container: {
      width: "100%",
      shadowOffset: { width: 0, height: 2 },
      shadowOpacity: 0.25,

      backgroundColor: theme.palette.screen.main,
      shadowColor: theme.palette.screen.contrast,
    },
    title: {
      borderBottomWidth: 1,
      borderBottomColor: theme.palette.screen.contrast,
    },
  })
}

✏️ Create src/design/Card/Card.test.tsx and update it to be:

import { render, screen } from "@testing-library/react-native"

import Typography from "../Typography"

import Card from "./Card"

describe("Design/Card", () => {
  it("renders", () => {
    render(
      <Card title="Hello!">
        <Typography variant="body">How are you?</Typography>
      </Card>,
    )

    expect(screen.getByText(/Hello/)).toBeOnTheScreen()
    expect(screen.getByText(/How are you?/)).toBeOnTheScreen()
  })
})

✏️ Create src/design/Card/index.ts and update it to be:

export { default } from "./Card"
export * from "./Card"

✏️ Create src/design/Screen/Screen.tsx and update it to be:

import Box, { BoxProps } from "../Box"
import { useTheme } from "../theme"

export interface ScreenProps extends BoxProps {
  noScroll?: boolean
}

const Screen: React.FC<ScreenProps> = ({
  noScroll = false,
  style,
  children,
  ...props
}) => {
  const theme = useTheme()

  return (
    <Box
      scrollable={!noScroll}
      style={{
        flex: 1,
        backgroundColor: theme.palette.screen.soft,
      }}
      {...props}
    >
      {children}
    </Box>
  )
}

export default Screen

✏️ Create src/design/Screen/Screen.test.tsx and update it to be:

import { render, screen } from "@testing-library/react-native"

import Typography from "../Typography"

import Screen from "./Screen"

describe("Design/Screen", () => {
  it("renders", () => {
    render(
      <Screen>
        <Typography variant="body">How are you?</Typography>
      </Screen>,
    )

    expect(screen.getByText(/How are you?/)).toBeOnTheScreen()
  })
})

✏️ Create src/design/Screen/index.ts and update it to be:

export { default } from "./Screen"
export * from "./Screen"

✏️ Update src/screens/StateList/StateList.tsx to be:

import { ScrollView } from "react-native"

import Box from "../../design/Box"
import Card from "../../design/Card"
import Screen from "../../design/Screen"
import Typography from "../../design/Typography"

import ListItem from "./components/ListItem"

const states = [
  {
    name: "Illinois",
    short: "IL",
  },
  {
    name: "Wisconsin",
    short: "WI",
  },
]

const StateList: React.FC = () => {
  return (
    <ScrollView>
      <Box padding="s">
        <Typography variant="heading">Place My Order: Coming Soon!</Typography>
      </Box>
      {states?.length ? (
        states.map((state) => <ListItem key={state.short} name={state.name} />)
      ) : (
        <Typography>No states found</Typography>
      )}
    </ScrollView>
  )
}

export default StateList

Exercise 3

We’ve provided three new design components for you: Button, Card, and Screen.

  • Update StateList to use our new Screen and Card components. (We’ll use Button later.)

Solution 3

If you’ve implemented the solution correctly, the tests will pass when you run npm run test!

Click to see the solution

✏️ Update src/screens/StateList/StateList.tsx to be:

import Card from "../../design/Card"
import Screen from "../../design/Screen"
import Typography from "../../design/Typography"

import ListItem from "./components/ListItem"

const states = [
  {
    name: "Illinois",
    short: "IL",
  },
  {
    name: "Wisconsin",
    short: "WI",
  },
]

const StateList: React.FC = () => {
  return (
    <Screen>
      <Card>
        <Typography variant="heading">Place My Order: Coming Soon!</Typography>
      </Card>
      {states?.length ? (
        states.map((state) => <ListItem key={state.short} name={state.name} />)
      ) : (
        <Typography>No states found</Typography>
      )}
    </Screen>
  )
}

export default StateList

Next steps

Next, let’s learn about how we can add dark mode with useState.