Building Custom Components page

Learn about components, the core building blocks of every React application.

Overview

In this section, you will:

  • Create components in React and structuring them properly.
  • Review how React components are (fundamentally) functions.
  • Use component props in React using TypeScript interfaces.
  • Structure code in the Modlets pattern.

Objective 1: Creating a custom component

Our App component currently shows our state list, but eventually we’ll want to show other page content. Let’s prepare now by moving all of the JSX from App to a new component called StateList.

What are components?

So far, we have placed all of our JSX inside the App function. Notice two things about the App function:

  1. The name starts with a capital letter.

  2. It returns something renderable (JSX).

const App: React.FC = () => {
  return <Text>Some page content</Text>
}

In React, we call this a component. When you create a component in React, you are creating building blocks that can be composed, reordered, and reused much like HTML elements.

React makes it relatively straightforward to create new components. Let’s learn to build our own.

Component structure

Let’s start by creating a component from a commonly reused element, the button.

First, React component names must start with a capital letter, so we can call this Button. By convention component names use PascalCase when naming components, so longer component names will look like IconButton. Avoid hyphens and underscores.

Second, our component must return either null or something renderable, like JSX. The return value of our components is almost always JSX, though JavaScript primitives like string and number are also valid. Components cannot return complex types like arrays or objects.

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

const PrimaryButton: React.FC = () => {
  return (
    <Pressable>
      <Text>Activate me</Text>
    </Pressable>
  )
}

Components are like small containers which can be reused throughout your application. The Button component above returns JSX and could then be rendered and reused by another component like App below.

const App: React.FC = () => {
  return (
    <>
      <Button />
      <Button />
      <Button />
    </>
  )
}

React Native components are functions

The JSX syntax allows function components to look like XML, but underneath they are still functions. The return of each component is unique and you can use the same component multiple times.

You can think of components as fancy functions.

While you can’t actually do the following, this is functionally similar to what React is doing for you.

const App: React.FC = () => {
  return (
    <>
      {Button()}
      {Button()}
      {Button()}
    </>
  )
}

Did you notice the FC that was used in the previous example to type the App const? Because we’re using TypeScript with our project, we can apply types to help make sure the function component is properly formed. React provides the type FC (Function component) that can be applied to a function component. This type defines the arguments and return value that a function component must implement.

Setup 1

✏️ Update App.tsx to be:

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

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

export const StateList: React.FC = () => {
  // Exercise: Update the `StateList` component to contain the logic that iterates over the `states` list.
}

const App: React.FC = () => {
  return (
    <SafeAreaView style={{ flex: 1 }}>
      <ScrollView>
        <View>
          <Text>Place My Order: Coming Soon!</Text>
        </View>
        <View>
          {/* Exercise: Use the new `StateList` component inside of the `App` component. */}
          {states?.length ? (
            states.map((state) => <Text key={state.short}>{state.name}</Text>)
          ) : (
            <Text>No states found</Text>
          )}
        </View>
      </ScrollView>
    </SafeAreaView>
  )
}

export default App

Verify 1

✏️ Update App.test.tsx to be:

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

import App, { StateList } from "./App"

describe("App", () => {
  it("renders", async () => {
    render(<App />)
    expect(screen.getByText(/Place my order/i)).toBeOnTheScreen()
  })

  it("renders states", async () => {
    render(<App />)
    expect(screen.getByText(/Illinois/i)).toBeOnTheScreen()
    expect(screen.getByText(/Wisconsin/i)).toBeOnTheScreen()
  })
})

describe("Screens/StateList", () => {
  it("renders", async () => {
    render(<StateList />)
    expect(screen.getByText(/Illinois/i)).toBeOnTheScreen()
    expect(screen.getByText(/Wisconsin/i)).toBeOnTheScreen()
  })
})

Exercise 1

  • Update the StateList component to contain the logic that iterates over the states list.
  • Use the new StateList component inside of the App component.

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 App.tsx to be:

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

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

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

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

export default App

Objective 2: Passing props

We’ve taken a great step to make our code more readable and our app more maintainable by creating the StateList component.

Let’s keep the good refactoring rolling by creating a ListItem component to house the JSX used to render each state in the list.

Using component props

In React, props are how we pass data from a parent component to a child component. Since function React components are fundamentally JavaScript functions, you can think of props like the arguments you pass to a function.

To clarify, props in React Native should not be confused with the properties on HTML elements in web development, even though they sound similar.

To receive props, function components must implement a React API that allows an optional object argument, usually referred to as props.

The properties on the props object—individually called a “prop”—can include whatever data the child component needs to make the component work. The property values can be any type, including functions and other React components.

We’re using TypeScript in our project, so we can create an interface for props and use it in the definition of a function component.

Let’s create a SubmitButton component to see props in action:

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

interface SubmitButtonProps {
  label: string
  onPress: () => void
}

const SubmitButton: React.FC<SubmitButtonProps> = (props) => {
  const { label, onPress } = props
  return (
    <Pressable onPress={onPress}>
      <Text>{label}</Text>
    </Pressable>
  )
}

In this example, SubmitButtonProps is an interface that defines the types for label (a string) and onPress (a function). Our SubmitButton component then uses these props to display a button with a label and a click action.

The example above illustrates how props are passed to component as an argument.

However, more commonly (and for the rest of this course) you will see props destructured in the function parameters:

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

interface SubmitButtonProps {
  label: string
  onPress: () => void
}

const SubmitButton: React.FC<SubmitButtonProps> = ({ label, onPress }) => {
  return (
    <Pressable onPress={onPress}>
      <Text>{label}</Text>
    </Pressable>
  )
}

Passing component props

Now, how do we use this SubmitButton? In JSX syntax a component’s props look like an HTML tag’s "attributes" and accept a value.

  • If a prop’s value type is a string, then the prop value is set using quotes.
  • Any other type of prop value is set using braces with the value inside.

In the example below, the label prop accepts a string. so the value is surrounded by double quotes. The onPress prop accepts a function, so the function value is surrounded by braces.

Here’s how to use our SubmitButton:

const content: React.FC = (
  <SubmitButton label="Activate" onPress={() => console.info("Activated!")} />
)

In the example above, we’re setting the label prop to the string “Activate” and the onPress prop to a function that displays an alert.

Reserved prop names

There are two prop names that you cannot use and are reserved by React:

  • children: this prop is automatically provided by React to every component. We will see this prop in later examples.

  • key: this prop is one you’ve seen before in the Introduction to JSX module! It’s not actually part of the component’s props in a traditional sense. Instead, it’s used by React itself to manage lists of elements and identify which items have changed, been added, or been removed.

Setup 2

✏️ Update App.tsx to be:

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

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

export interface ListItemProps {
  // Exercise: Update the `ListItemProps` type to enforce a `name` prop of the appropriate primitive type.
}

export const ListItem: React.FC = () => {
  // Exercise: Update the `ListItem` component to use the `ListItemProps` and return the `<Text>` element.
}

export const StateList: React.FC = () => {
  // Exercise: Update the `StateList` component to use the `ListItem` component handle each state item.
  return (
    <ScrollView>
      <View>
        <Text>Place My Order: Coming Soon!</Text>
      </View>
      <View>
        {states?.length ? (
          states.map((state) => <Text key={state.short}>{state.name}</Text>)
        ) : (
          <Text>No states found</Text>
        )}
      </View>
    </ScrollView>
  )
}

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

export default App

Verify 2

✏️ Update App.test.tsx to be:

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

import App, { StateList, ListItem } from "./App"

describe("App", () => {
  it("renders", async () => {
    render(<App />)
    expect(screen.getByText(/Place my order/i)).toBeOnTheScreen()
  })

  it("renders states", async () => {
    render(<App />)
    expect(screen.getByText(/Illinois/i)).toBeOnTheScreen()
    expect(screen.getByText(/Wisconsin/i)).toBeOnTheScreen()
  })
})

describe("Screens/StateList", () => {
  it("renders", async () => {
    render(<StateList />)
    expect(screen.getByText(/Illinois/i)).toBeOnTheScreen()
    expect(screen.getByText(/Wisconsin/i)).toBeOnTheScreen()
  })
})

describe("Screens/StateList/ListItem", () => {
  it("renders", async () => {
    render(<ListItem name="This is a given name prop." />)
    expect(
      screen.getByText(/This is a given name prop./i, { exact: false }),
    ).toBeOnTheScreen()
  })
})

Exercise 2

  • Update the ListItemProps type to enforce a name prop of the appropriate primitive type.
  • Update the ListItem component to use the ListItemProps and returns the <Text> element.
  • Update the StateList component to use the ListItem component to handle each state item.

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 App.tsx to be:

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

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

export interface ListItemProps {
  name: string
}

export const ListItem: React.FC<ListItemProps> = ({ name }) => {
  return <Text>{name}</Text>
}

export 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>
  )
}

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

export default App

Objective 3: Organize code with the Modlet pattern

Our efforts to refactor have been going swimmingly; however, the App.tsx file is getting a bit crowded. It’s about time we take the time to organize our code, while doing so we’ll also introduce a useful pattern to follow.

Modlets

Modlets are a folder/file structure that place a heavy emphasis on the idea of abstraction.

They are completely self contained; each modlet is basically its own application. It has everything it needs inside of it.

Each modlet is treated as a black box that can only be accessed through a single point: its index file.

Modlets follow these three rules:

  1. Only import folders not specific files.
  2. Do not reach “up” into parent directories or “sideways” into sibling directories to import things. Only “down” to child modlets, or to designated shared modlets in a parent.
  3. Each modlet is a “black box” that controls what it exports through its index file. Anything exported in the index should be stable: Don’t change those api’s unless you have to!

These will make sense as we go along.

Modlets example

We’re building a simple product page for an e-commerce site. It has three components:

  • Product Details.
  • Image Carousel.
  • Add To Cart Button.

Without a plan of how to organize our files, we might end up with something like this:

  • src/
    • ProductPage/
      • CartButton.tsx
      • CartButton.test.tsx
      • ImageCarousel.tsx
      • ImageCarousel.test.tsx
      • placeholderImage.png
      • ProductPage.tsx
      • ProductDetails.tsx
      • ProductDetails.test.ts

The same page, refactored into a modlet architecture would look like this:

  • src/
    • ProductPage/
      • ProductPage.tsx
      • index.ts
      • components/
        • assets/
          • placeholderImage.png
        • ProductDetails/
          • index.ts
          • ProductDetails.tsx
          • ProductDetails.test.ts
        • ImageCarousel/
          • index.ts
          • ImageCarousel.tsx
          • ImageCarousel.test.ts
        • CartButton/
          • index.ts
          • CartButton.tsx
          • CartButton.test.ts

Compared to the former structure, the modlet structure is:

  • Clearly organized.
  • Location of relevant code is clear.
  • Related functionality bundled in same folder.

Writing Modlet components

For more clarity, the ProductDetails component has a default export.

const ProductDetails: React.FC = () => {
  return (
    <View>
      <Text>Here are some details about this product!</Text>
    </View>
  )
}

export default ProductDetails

Then, we have the index file for this modlet. It re-exports the default from the key implementation file form that modlet, in this case ProductDetails.

export { default } from "./ProductDetails"

Importing Modlets

With the Modlet structure, the top-level component’s imports statements look as follows:

import CartButton from "./components/CartButton"
import ImageCarousel from "./components/ImageCarousel"
import ProductDetails from "./components/ProductDetails"

const ProductPage: React.FC = (props) => {
  return (
    <>
      <ImageCarousel />
      <ProductDetails />
      <CartButton />
    </>
  )
}

  • Importing folders, not specific files.
  • Modlet remains a black box.
  • Every import is from a child of the top level modlet. Only reaching "down" for imports!

Notice that we don’t import specific files, but rather the modlet we want to use.

This allows the modlet to maintain control over what it exposes.

Also, every import is from a child of that top level modlet. We are only reaching down to grab imports!

Setup 3

It’s best practice to create a new folder that will contain all of the related files for each component, including test files.

✏️ Create src/ (folder)

✏️ Move App.tsx to src/App.tsx

✏️ Update index.js to be:

/**
 * @format
 */

import { AppRegistry } from "react-native"

import { name as appName } from "./app.json"
import App from "./src/App"

AppRegistry.registerComponent(appName, () => App)

✏️ Create src/screens/StateList/ (folder)

✏️ Create src/screens/StateList/StateList.tsx and update it to be:

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

import ListItem from "./components/ListItem"

// Exercise: Move the `StateList` component logic here.

export default StateList

✏️ Create src/screens/StateList/index.ts and update it to be:

export { default } from "./StateList"

✏️ Create src/screens/StateList/components/ (folder)

✏️ Create src/screens/StateList/components/ListItem/ (folder)

✏️ Create src/screens/StateList/components/ListItem/ListItem.tsx and update it to be:

import { Text } from "react-native"

// Exercise: Move the `ListItem` component logic here.

export default ListItem

✏️ Create src/screens/StateList/components/ListItem/index.tsx and update it to be:

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

Verify 3

✏️ Move App.test.tsx to src/App.test.tsx and update it to be:

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

import App from "./App"

describe("App", () => {
  it("renders", async () => {
    render(<App />)
    expect(screen.getByText(/Place my order/i)).toBeOnTheScreen()
  })

  it("renders states", async () => {
    render(<App />)
    expect(screen.getByText(/Illinois/i)).toBeOnTheScreen()
    expect(screen.getByText(/Wisconsin/i)).toBeOnTheScreen()
  })
})

✏️ Create src/screens/StateList/StateList.test.tsx and update it to be:

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

import StateList from "./StateList"

// Exercise: Move the `StateList` test logic here.

✏️ Create src/screens/StateList/components/ListItem/ListItem.test.tsx and update it to be:

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

import ListItem from "./ListItem"

// Exercise: Move the `ListItem` test logic here.

Exercise 3

  • Move the StateList and ListItem component logic into the correct file.
  • Make sure to properly update each import and to reference every component’s essential files.

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/App.tsx to be:

import { SafeAreaView } from "react-native"

import StateList from "./screens/StateList"

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

export default App

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

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

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

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

import { Text } from "react-native"

export interface ListItemProps {
  name: string
}

const ListItem: React.FC<ListItemProps> = ({ name }) => {
  return <Text>{name}</Text>
}

export default ListItem

Next steps

Next we will learn about debugging by using React Devtools.