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:
- Context: Think of the context as a box of things. The box needs to be available to all who want to use it.
- Provider: The provider puts things into the box. Whatever data it handles is only available to its children.
- 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 providedThemeContext
. - 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 useBox
andTypography
instead ofView
andText
.
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:
- Page/Screen/etc. A main view of the app, which will often have a prop like
title
. - Form components like TextInput, Select, Button. Almost every app will need forms of some kind.
- Typography. The basic text component, to make it easier to standardize text size and color.
- Grid. Most apps will need to control placement of elements on the screen.
- 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 newScreen
andCard
components. (We’ll useButton
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.