Thinking in Components and Building Reusable UI Blocks

Capítulo 2

Estimated reading time: 14 minutes

+ Exercise

What “Thinking in Components” Means

In React Native, you build screens by composing small pieces of UI called components. “Thinking in components” is the habit of looking at a design (or an existing screen) and breaking it down into independent, reusable blocks. Each block has a clear responsibility, receives data through props, and can be combined with other blocks to form larger sections and complete screens.

A useful mental model is: a component is a function that turns data into UI. When you adopt this model, you stop building one-off screens and start building a library of UI blocks that can be reused across the app. This reduces duplication, makes changes safer, and helps you scale the codebase as the number of screens grows.

Components as Building Blocks

Most screens can be decomposed into a hierarchy. For example, a “Product List” screen might contain a header, a search bar, a list, and list items. Each list item might contain an image, a title, a price, and an “Add” button. Each of those could be a component if it appears in multiple places or if it has enough complexity to deserve its own file.

Thinking in components is not only about splitting code into many files. It is about choosing boundaries that make components easy to understand, easy to test, and easy to reuse.

How to Identify Components in a UI

When you look at a UI mockup, you can systematically find components by asking a few questions.

Continue in our app.
  • Listen to the audio with the screen off.
  • Earn a certificate upon completion.
  • Over 5000 courses for you to explore!
Or continue reading below...
Download App

Download the app

1) What repeats?

Anything that repeats is a strong candidate for a reusable component. Examples include list rows, cards, chips/tags, buttons with a consistent style, and form fields.

2) What has a single responsibility?

A component should do one job well. A “UserAvatar” component should focus on rendering an avatar (image or initials) and maybe a status badge. It should not also fetch user data, format dates, and handle navigation. Keeping responsibilities narrow makes reuse easier.

3) What changes independently?

If one part of the UI changes frequently without affecting the rest, it may deserve its own component. For instance, a “LikeButton” that toggles state can be isolated from the rest of a post card.

4) What is purely presentational vs. what is stateful?

A common approach is to separate presentational components (mostly props in, UI out) from container components (manage state, data fetching, and orchestration). In React Native, you can keep most reusable UI blocks presentational and let screens or higher-level components manage state.

Designing Reusable Components: Props, Variants, and Composition

Props: The Component’s API

Props are the public interface of a component. A well-designed component has props that are easy to understand and hard to misuse. Prefer explicit props over “magic” behavior.

For example, a reusable button might accept label, onPress, variant, and disabled. Avoid passing a large “options” object unless there is a clear reason, because it can hide what the component actually needs.

Variants: One Component, Multiple Looks

Reusable components often need to support a few visual or behavioral variations. Instead of creating separate components like PrimaryButton, SecondaryButton, and DangerButton, you can use a single Button with a variant prop. This keeps the API consistent and reduces duplication.

Composition: Prefer Children Over Too Many Props

When a component needs to render arbitrary content, use children. This is especially useful for layout components like Card, Section, or Row. Composition keeps components flexible without adding many specialized props.

For example, a Card component can render a container with padding and border radius, and you can place any content inside it. This is often better than adding props like title, subtitle, leftIcon, rightIcon, footerText, and so on, unless the card is truly a standardized pattern across the app.

Practical Step-by-Step: Build a Small Reusable UI Kit

This section walks through building a set of reusable components you can use across multiple screens: AppButton, Card, Avatar, and ProductRow. The goal is not to create a full design system, but to practice choosing boundaries and designing component APIs.

Step 1: Create a Shared Folder Structure

Organize reusable UI blocks in a predictable place. One common approach is:

  • src/components/ for reusable components
  • src/components/ui/ for low-level UI primitives (buttons, cards, text wrappers)
  • src/components/features/ for feature-specific components (product row, user header)

Even if your app is small, this structure helps you avoid mixing screen code with reusable blocks.

Step 2: Build a Reusable Button Component

A button is a classic reusable component because it appears everywhere and should look consistent. In React Native, you can base it on Pressable for better control over pressed states.

import React from 'react';import { Pressable, Text, StyleSheet, ActivityIndicator } from 'react-native';export function AppButton({  label,  onPress,  variant = 'primary',  disabled = false,  loading = false,}) {  const isDisabled = disabled || loading;  return (    <Pressable      onPress={onPress}      disabled={isDisabled}      style={({ pressed }) => [        styles.base,        styles[variant],        isDisabled && styles.disabled,        pressed && !isDisabled && styles.pressed,      ]}    >      {loading ? (        <ActivityIndicator color={variant === 'primary' ? '#fff' : '#111'} />      ) : (        <Text style={[styles.label, styles[`label_${variant}`]]}>{label}</Text>      )}    </Pressable>  );}const styles = StyleSheet.create({  base: {    paddingVertical: 12,    paddingHorizontal: 16,    borderRadius: 10,    alignItems: 'center',    justifyContent: 'center',  },  primary: {    backgroundColor: '#2f6fed',  },  secondary: {    backgroundColor: '#eef2ff',    borderWidth: 1,    borderColor: '#c7d2fe',  },  danger: {    backgroundColor: '#dc2626',  },  disabled: {    opacity: 0.5,  },  pressed: {    transform: [{ scale: 0.99 }],  },  label: {    fontSize: 16,    fontWeight: '600',  },  label_primary: {    color: '#fff',  },  label_secondary: {    color: '#111827',  },  label_danger: {    color: '#fff',  },});

Key ideas in this component:

  • It exposes a small, clear API: label, onPress, variant, disabled, loading.
  • It centralizes styling so buttons are consistent across the app.
  • It handles pressed feedback and disabled behavior internally.

Step 3: Build a Card Component Using Composition

A card is a layout primitive: it wraps content with a consistent container style. This is a great place to use children.

import React from 'react';import { View, StyleSheet } from 'react-native';export function Card({ children, style }) {  return <View style={[styles.card, style]}>{children}</View>;}const styles = StyleSheet.create({  card: {    backgroundColor: '#fff',    borderRadius: 14,    padding: 14,    borderWidth: 1,    borderColor: '#e5e7eb',  },});

Notice that Card accepts an optional style prop to allow small adjustments without changing the component. This is a common pattern in reusable UI blocks: provide a default look, but allow controlled customization.

Step 4: Build an Avatar Component with a Fallback

Avatars often need to handle missing images. A reusable avatar component can accept uri, name, and size, and show initials when no image exists.

import React from 'react';import { View, Text, Image, StyleSheet } from 'react-native';function getInitials(name = '') {  const parts = name.trim().split(' ').filter(Boolean);  if (parts.length === 0) return '?';  const first = parts[0][0] || '';  const last = parts.length > 1 ? parts[parts.length - 1][0] : '';  return (first + last).toUpperCase();}export function Avatar({ uri, name, size = 40 }) {  const initials = getInitials(name);  const radius = size / 2;  return (    <View style={[styles.container, { width: size, height: size, borderRadius: radius }]}>      {uri ? (        <Image          source={{ uri }}          style={{ width: size, height: size, borderRadius: radius }}        />      ) : (        <Text style={styles.initials}>{initials}</Text>      )}    </View>  );}const styles = StyleSheet.create({  container: {    backgroundColor: '#e5e7eb',    alignItems: 'center',    justifyContent: 'center',    overflow: 'hidden',  },  initials: {    fontWeight: '700',    color: '#111827',  },});

Why this is reusable:

  • It does not know where the user data comes from.
  • It handles a common edge case (missing image) consistently.
  • It can be used in headers, lists, and profile screens.

Step 5: Build a Feature Component: ProductRow

Feature components are still reusable, but within a specific domain. A ProductRow can be reused in multiple lists: search results, favorites, category pages, and so on.

Try to keep it presentational: accept a product object and callbacks like onPress and onAdd.

import React from 'react';import { View, Text, Image, Pressable, StyleSheet } from 'react-native';import { AppButton } from '../ui/AppButton';import { Card } from '../ui/Card';export function ProductRow({ product, onPress, onAdd }) {  return (    <Pressable onPress={() => onPress?.(product)}>      <Card style={styles.card}>        <View style={styles.row}>          <Image source={{ uri: product.imageUrl }} style={styles.image} />          <View style={styles.info}>            <Text style={styles.title} numberOfLines={1}>{product.title}</Text>            <Text style={styles.subtitle} numberOfLines={2}>{product.subtitle}</Text>            <View style={styles.bottom}>              <Text style={styles.price}>${product.price.toFixed(2)}</Text>              <AppButton                label="Add"                variant="secondary"                onPress={() => onAdd?.(product)}              />            </View>          </View>        </View>      </Card>    </Pressable>  );}const styles = StyleSheet.create({  card: {    marginBottom: 12,  },  row: {    flexDirection: 'row',    gap: 12,  },  image: {    width: 72,    height: 72,    borderRadius: 12,    backgroundColor: '#f3f4f6',  },  info: {    flex: 1,  },  title: {    fontSize: 16,    fontWeight: '700',    color: '#111827',  },  subtitle: {    marginTop: 2,    fontSize: 13,    color: '#4b5563',  },  bottom: {    marginTop: 10,    flexDirection: 'row',    alignItems: 'center',    justifyContent: 'space-between',    gap: 10,  },  price: {    fontSize: 15,    fontWeight: '700',    color: '#111827',  },});

Component boundary choices here:

  • ProductRow uses Card and AppButton rather than re-implementing their styles.
  • It accepts callbacks so the parent decides what happens when a user taps the row or presses “Add”.
  • It keeps formatting minimal and predictable (for example, price formatting). If formatting becomes complex, extract it to a utility function.

Step-by-Step: Refactor a Screen into Components

Suppose you have a screen that renders a list of products with a header and a filter bar. A common beginner pattern is to put everything in one file. Refactoring is the practice of extracting reusable blocks without changing behavior.

Step 1: Start with the “largest obvious” extraction

Extract parts that are clearly independent, like a header area. If the header is used on multiple screens, make it reusable. If it is unique, keep it local but still extracted for readability.

Step 2: Extract repeated list items

If you are mapping over data and returning a chunk of JSX, that chunk is a prime candidate for a component. Replace the inline JSX with <ProductRow /> or a similar component.

Step 3: Replace duplicated styles with shared primitives

If multiple components use the same container styling (rounded border, padding, light border), introduce a Card primitive. If multiple screens use the same button style, introduce AppButton.

Step 4: Define clear props and remove hidden dependencies

A reusable component should not rely on variables from the parent scope. Everything it needs should come from props (or from internal state that is truly internal). If you find a component reading from a parent’s state directly, you have not actually extracted a component; you have moved code into another file.

Rules of Thumb for Component Boundaries

Keep components small, but not fragmented

Very small components can become noise if they only wrap a single View with no logic and no reuse. Extract when it improves clarity, reuse, or testability. If a component is only used once and is trivial, it may not need its own file.

Prefer “data in, events out”

Reusable UI components should generally follow this pattern:

  • They receive data via props (text, numbers, booleans, objects).
  • They render UI deterministically based on those props.
  • They expose user actions via callbacks (for example, onPress, onChangeText).

This makes components predictable and easy to reuse in different contexts.

Avoid overloading components with too many responsibilities

If a component both fetches data and renders complex UI, it becomes harder to reuse. Consider splitting it into:

  • A container component that handles state and data.
  • A presentational component that renders UI based on props.

Even if you do not create separate files, you can keep this separation conceptually by extracting presentational parts.

Make defaults sensible

Reusable components should work with minimal configuration. For example, Avatar should have a default size. AppButton should default to a primary variant. Defaults reduce boilerplate and encourage consistent usage.

Common Reusability Patterns in React Native

1) “Wrapper” components for consistent spacing

Spacing is often inconsistent when each screen sets its own padding and margins. A wrapper like Screen or Section can standardize padding and background color. This is a reusable layout block rather than a visual widget.

2) Controlled inputs

Form fields are a frequent source of duplication. A reusable input component can accept value, onChangeText, label, error, and placeholder. Keep it controlled so the parent owns the state, which makes it easier to integrate with validation and submission logic.

3) Empty states and loading states

Lists often need an empty state (“No results”) and a loading state. Instead of re-creating these each time, build small components like EmptyState and InlineLoader. They improve consistency and reduce the chance of forgetting edge cases.

4) Render props and component injection

Sometimes you want a reusable component to allow customization without knowing the details. For example, a list component might accept a renderItem function. This is a powerful pattern, but for basic apps, start with simpler composition via children and only introduce render props when needed.

Practical Checklist: Is This Component Reusable?

  • Does it have a clear name that matches what it renders (for example, ProductRow, Avatar, AppButton)?
  • Can it be used in at least two places without modification?
  • Are its props minimal and explicit?
  • Does it avoid depending on external variables from a specific screen?
  • Does it expose user actions via callbacks instead of handling app-level behavior internally?
  • Does it handle common edge cases (missing image, disabled state, loading state) in a consistent way?

Putting It Together: Example Usage

Below is an example of how these reusable blocks might be used together in a screen component. The screen orchestrates data and actions, while the UI blocks focus on rendering.

import React, { useMemo, useState } from 'react';import { View, Text, FlatList, StyleSheet } from 'react-native';import { ProductRow } from '../components/features/ProductRow';import { Avatar } from '../components/ui/Avatar';import { AppButton } from '../components/ui/AppButton';export function ProductsScreen() {  const [cartIds, setCartIds] = useState(new Set());  const products = useMemo(() => [    { id: '1', title: 'Coffee Beans', subtitle: 'Medium roast, 1 lb', price: 12.99, imageUrl: 'https://picsum.photos/200' },    { id: '2', title: 'Ceramic Mug', subtitle: '12 oz, matte finish', price: 9.5, imageUrl: 'https://picsum.photos/201' },  ], []);  function handleAdd(product) {    setCartIds(prev => new Set(prev).add(product.id));  }  return (    <View style={styles.container}>      <View style={styles.header}>        <View style={styles.headerLeft}>          <Avatar name="Alex Johnson" size={36} />          <View>            <Text style={styles.greeting}>Welcome back</Text>            <Text style={styles.name}>Alex</Text>          </View>        </View>        <AppButton label="Checkout" variant="primary" onPress={() => {}} />      </View>      <FlatList        data={products}        keyExtractor={(item) => item.id}        renderItem={({ item }) => (          <ProductRow            product={item}            onPress={() => {}}            onAdd={handleAdd}          />        )}      />      <Text style={styles.cartInfo}>Items in cart: {cartIds.size}</Text>    </View>  );}const styles = StyleSheet.create({  container: { flex: 1, padding: 16, backgroundColor: '#f9fafb' },  header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 },  headerLeft: { flexDirection: 'row', alignItems: 'center', gap: 10 },  greeting: { fontSize: 12, color: '#6b7280' },  name: { fontSize: 16, fontWeight: '700', color: '#111827' },  cartInfo: { marginTop: 8, color: '#111827' },});

Notice how the screen reads like a high-level layout description. The details of button styling, card styling, and avatar fallback logic are hidden inside reusable components. This is the practical benefit of thinking in components: you move complexity into well-named blocks, and screens become easier to maintain.

Common Mistakes and How to Avoid Them

Making components too specific too early

If you hard-code text, colors, or behavior that should vary, the component becomes difficult to reuse. Start with a simple API and add props only when you have a real use case. For example, do not add five variants to a button until you actually need them.

Passing entire app state into small components

A component that receives a huge object like appState is not reusable; it is coupled to the app. Pass only what the component needs. This makes the component easier to understand and reduces unnecessary re-renders.

Overusing “style” overrides

Allowing a style prop is useful, but if every usage requires heavy overrides, your base component is not well-designed. In that case, consider adding a small number of explicit props (like variant or size) to cover common differences, and keep style for minor tweaks.

Mixing layout and business logic

Reusable UI blocks should not decide business rules like “only premium users can add to cart.” Instead, the parent should decide whether to show the button, disable it, or display a message. Keep UI blocks focused on rendering and emitting events.

Now answer the exercise about the content:

Which approach best follows the idea of building reusable React Native components?

You are right! Congratulations, now go to the next page

You missed! Try again.

Reusable components are easiest to share when they follow data in, events out: props in, deterministic UI out, and callbacks for actions. State and data fetching are typically handled by screens or container components.

Next chapter

Props and State for Data Flow and Interactive Screens

Arrow Right Icon
Free Ebook cover React Native Basics: Components, Styling, and Navigation Concepts
17%

React Native Basics: Components, Styling, and Navigation Concepts

New course

12 pages

Download the app to earn free Certification and listen to the courses in the background, even with the screen off.