Reusable Components and Context
Introduction
This is lesson 1 of chapter 6, aligned with course 6 of the Meta Front-End Developer pathway ("Advanced React", domain advanced-react). In chapter 5 you built components that receive props and keep local state. As an interface grows, two problems appear: the same markup is copied in many places, and some data (the signed-in member, the language, the colour theme) is needed by components buried deep in the tree.
This lesson teaches how to design components that are reusable (configurable through props, children, and sensible defaults) and how to share data without passing it through every level with context. The scenario is original: Harbor Workshops needs a shared card, a modal-style dialog, a theme switch, and a member session that many components read. By the end you will be able to decide when props are enough, when context is the better tool, and how to avoid the performance and design traps of both.
Learning objectives
- Design a reusable component with a small, predictable props interface.
- Use
childrenand named "slot" props to let callers supply content. - Apply default values, prop spreading, and prop validation responsibly.
- Recognise prop drilling and decide whether context is justified.
- Create a context, provide a value, and consume it with
useContext. - Wrap a context in a provider component and a custom hook with a clear error.
- Explain why every consumer re-renders when the provided value changes, and limit the damage.
- Choose between lifting state, composition, and context for a given problem.
1. What makes a component reusable
A component is reusable when callers can adapt it without editing its source. In React the adaptation channels are props, children, and (in this lesson) context. A good reusable component has these properties:
- A narrow purpose.
WorkshopCardshows a workshop; it does not also fetch data and manage the cart. - Data in, events out. It receives values as props and reports user actions through callback props such as
onReserve. - No hidden dependencies. It does not read a global variable or call an API on its own unless that is its stated job.
- Sensible defaults. Optional props have defaults so the simplest usage is short.
A first reusable card
function Card({ title, footer, children, variant = 'plain' }) {
return (
<section className={`card card--${variant}`}>
<h3 className="card__title">{title}</h3>
<div className="card__body">{children}</div>
{footer && <div className="card__footer">{footer}</div>}
</section>
);
}
The component does not know what it contains. Callers decide:
<Card title="Sourdough basics" variant="highlight" footer={<button>Reserve</button>}>
<p>Saturday, 10:00. Four seats left.</p>
</Card>
<Card title="Studio rules">
<ul>
<li>Wear closed shoes.</li>
<li>Arrive ten minutes early.</li>
</ul>
</Card>
Notice three techniques in the definition:
childrenis the content placed between the opening and closing tags. It can be text, elements, arrays, or nothing.- A slot prop (
footer) accepts JSX through a named prop. Use slots when a component has several distinct areas (header, footer, actions) andchildrenalone would be ambiguous. - A default (
variant = 'plain') means the caller may omit it. The default is applied only when the prop isundefined, not when it isnullor0.
Props design rules
| Rule | Why it matters |
|---|---|
| Prefer few props with clear names | Each prop is part of the component's public contract |
| Pass data, not pre-rendered strings, when the component formats it | The component can change formatting without touching callers |
Name callbacks onSomething and handlers handleSomething |
Callers immediately see what is an event |
Avoid boolean prop explosions (isBig isRed isRounded) |
Use one variant or size prop with a few named values |
| Do not mutate props | Props are read-only; copy before changing |
Keep key and ref in mind |
key is not a prop; in React 19, ref can be passed to a function component as an ordinary prop |
Spreading props with care
A wrapper around a native element often needs to forward extra attributes.
function TextField({ label, id, ...inputProps }) {
return (
<div className="field">
<label htmlFor={id}>{label}</label>
<input id={id} {...inputProps} />
</div>
);
}
<TextField id="email" label="Email" type="email" required autoComplete="email" />
The rest syntax collects every prop except label and id into inputProps, then spreads them onto the <input>. This keeps the wrapper small while supporting type, required, value, onChange, and any ARIA attribute. The trade-off is that callers can override anything, so place explicit props after the spread when the wrapper must win: <input {...inputProps} id={id} />.
Worked example: refactoring duplicated markup
Statement. Harbor Workshops has two nearly identical blocks, one for workshops and one for instructors. Both show a title, an image, a paragraph, and an optional action button. The two copies already differ by a typo in a class name. Refactor them into one reusable component.
Step 1: find what varies. The title, image source and alt text, the paragraph, and whether a button appears. Everything else (layout classes, heading level) is identical.
Step 2: define the interface.
function MediaCard({ title, imageSrc, imageAlt, children, action }) {
return (
<article className="media-card">
<img src={imageSrc} alt={imageAlt} loading="lazy" />
<div>
<h3>{title}</h3>
{children}
{action}
</div>
</article>
);
}
Step 3: replace both copies.
<MediaCard title="Wheel pottery" imageSrc="/img/pottery.jpg" imageAlt="Hands shaping clay on a wheel"
action={<button onClick={() => reserve('w2')}>Reserve</button>}>
<p>Learn to centre clay and pull walls.</p>
</MediaCard>
<MediaCard title="Maya Ortiz" imageSrc="/img/maya.jpg" imageAlt="Maya Ortiz in the pottery studio">
<p>Ceramicist with twelve years of teaching experience.</p>
</MediaCard>
Step 4: why the alternatives are weaker. A single component with a type="workshop" | "instructor" prop and if branches inside would couple the component to every future card type. Passing the button as a boolean showButton plus buttonLabel plus onButtonClick would multiply props. Passing the action as a slot keeps the card unaware of what the action does.
2. Prop drilling and when it hurts
Prop drilling means passing a prop through several components that do not use it, only to reach one that does. Suppose the signed-in member's name must appear in a header badge deep inside the layout:
<App user={user}>
<Layout user={user}>
<Header user={user}>
<UserBadge user={user} />
</Header>
</Layout>
</App>
Layout and Header do not care about user; they only forward it. Every new intermediate component must remember to pass it along, and renaming the prop touches four files.
Before reaching for context, try the lighter solutions:
- Composition. Instead of passing
userdown, create the<UserBadge user={user} />element high up and pass it aschildrenor a slot.Layoutthen receives finished JSX and never seesuser. - Moving state closer. If only one subtree needs the data, keep the state there.
- Context. Use it when many components at different depths need the same value and composition would make the structure awkward.
3. Context in practice
Context lets a parent make a value available to any descendant without passing props. There are three steps: create, provide, consume.
Step 1: create the context
import { createContext } from 'react';
export const ThemeContext = createContext('light');
The argument is the default value, used only when a component reads the context without a matching provider above it.
Step 2: provide a value
function App() {
const [theme, setTheme] = useState('light');
const value = { theme, toggle: () => setTheme(t => (t === 'light' ? 'dark' : 'light')) };
return (
<ThemeContext value={value}>
<Layout />
</ThemeContext>
);
}
In React 19 you can render the context object itself as the provider (<ThemeContext value={...}>). In earlier versions, and in many existing tutorials and codebases, you will see <ThemeContext.Provider value={...}>. Both forms mean the same thing; recognise the older one because most real projects still use it.
Step 3: consume the value
import { useContext } from 'react';
function ThemeButton() {
const { theme, toggle } = useContext(ThemeContext);
return <button onClick={toggle}>Theme: {theme}</button>;
}
useContext returns the value from the nearest provider above the component. If the provider's value changes, React re-renders every component that read that context. Layout and Header are untouched by the change in code and do not need props.
The provider component and the custom hook
Exposing the raw context object invites mistakes (reading it outside a provider, forgetting a field). The standard pattern wraps both sides:
const SessionContext = createContext(null);
export function SessionProvider({ children }) {
const [member, setMember] = useState(null);
const value = useMemo(
() => ({
member,
signIn: name => setMember({ name }),
signOut: () => setMember(null),
}),
[member]
);
return <SessionContext value={value}>{children}</SessionContext>;
}
export function useSession() {
const ctx = useContext(SessionContext);
if (ctx === null) {
throw new Error('useSession must be used inside <SessionProvider>');
}
return ctx;
}
Usage:
root.render(
<SessionProvider>
<App />
</SessionProvider>
);
function Header() {
const { member, signOut } = useSession();
return member ? <button onClick={signOut}>Sign out {member.name}</button> : <a href="/login">Sign in</a>;
}
Benefits: components import one hook, the context object stays private, the error message tells a developer exactly what went wrong, and you can change how the session is stored without touching consumers.
Why useMemo appears in the provider
Every time SessionProvider renders, an object literal { member, signIn, signOut } would be a new object, even if member is unchanged. React compares context values with Object.is, so a new object means "changed", and every consumer re-renders. useMemo returns the same object while member is unchanged. This matters when the provider re-renders for reasons unrelated to the value (for example, its parent re-rendered). With the React Compiler enabled, such memoisation may be applied automatically, but you should still understand the cause.
Splitting contexts
A single big context holding the member, the theme, the cart, and the language forces everything to re-render when any part changes. Prefer several small contexts, each with one concern, and nest the providers:
<SessionProvider>
<ThemeProvider>
<CartProvider>
<App />
</CartProvider>
</ThemeProvider>
</SessionProvider>
When a value changes often (cart quantities) and another rarely (theme), separating them keeps the rarely changing consumers still.
Overriding context for a subtree
A provider can be placed deeper to give one part of the tree a different value. A dark-themed sidebar inside a light page:
<ThemeContext value={{ theme: 'dark', toggle: noop }}>
<Sidebar />
</ThemeContext>
Components inside Sidebar read 'dark'; the rest of the page still reads the outer value.
4. Choosing the right tool
| Situation | Best choice | Reason |
|---|---|---|
| Two siblings need the same value | Lift state to the common parent | Simple and explicit |
| A value travels through 1 or 2 layers | Pass props | Easy to trace, no indirection |
| A wrapper must show arbitrary content | children or slot props |
Keeps the wrapper unaware of the content |
| Many components at different depths read the same, slowly changing value | Context | Avoids drilling |
| Frequently updated, complex data shared widely | Context plus reducer, or an external state library | Plain context re-renders all consumers |
| Server data (lists loaded from an API) | A data-fetching approach, not context alone | Needs loading, error and cache handling |
Context is not a state manager. It transports a value; the state still lives in useState or useReducer inside the provider.
Common pitfalls
- Creating the value inline without memoisation in a provider that re-renders often, causing every consumer to re-render.
- Reading context outside a provider and silently using the default value. The
nulldefault plus a throwing hook makes this bug obvious. - Putting everything into one context. Split by concern and update frequency.
- Using context to avoid designing props. If only two layers are involved, props are clearer.
- Mutating the context value (for example
session.member.name = 'x'). Always update through state setters. - Forgetting
children. A wrapper that does not render{children}shows an empty box with no error. - Passing JSX in a prop and expecting it to be a component.
footer={<button/>}passes an element; to pass a component type, use a capitalised variable and render<Footer />. - Adding too many
...restspreads, which can leak unknown attributes onto DOM elements and trigger React warnings.
Exam tips
- When a question describes data needed by many deep components with intermediate components that do not use it, the expected answer is context (or composition), not "make it global".
- If a question says "all consumers re-render unexpectedly", look for a provider value created inline on every render.
- Remember: the default value of
createContextapplies only when there is no provider above; a provider withvalue={undefined}suppliesundefined, not the default. childrenis just a prop namedchildren. Questions may show it passed explicitly as an attribute.
Mini case study: the theme that does not switch
Situation. A developer builds a theme switch. The button toggles state, but the footer, which is rendered outside the provider, never changes colour.
function App() {
return (
<>
<ThemeProvider>
<Header />
<Main />
</ThemeProvider>
<Footer />
</>
);
}
function Footer() {
const { theme } = useTheme();
return <footer className={`footer footer--${theme}`}>Harbor Workshops</footer>;
}
The footer always shows the default light theme.
Analysis. useContext reads the nearest provider above the component. Footer is a sibling of ThemeProvider, not a descendant, so no provider is above it and it receives the context's default value. If the hook throws on a missing provider, the app would crash instead; here the default 'light' object hides the bug.
Fix. Move Footer inside the provider:
<ThemeProvider>
<Header />
<Main />
<Footer />
</ThemeProvider>
Follow-up improvement. Change the context default to null and make useTheme throw when it is null. The original mistake would then have produced a clear error message during development. This is why the lesson recommends a guarded custom hook.
Why other fixes are worse. Passing theme to Footer as a prop would work but reintroduces drilling for every new component. Storing the theme in a global variable would not trigger re-renders when it changes.
Key terms
- Reusable component: a component configured through props and
childrenrather than edited per use. children: the content nested between a component's tags, received as a prop.- Slot prop: a named prop that accepts JSX for a specific area of a component.
- Prop drilling: passing props through components that do not use them.
- Context: a mechanism for making a value available to a whole subtree.
- Provider: the element that supplies a context value to its descendants.
- Consumer: a component that reads a context, usually via
useContext. - Default value: the value used when no provider is above the consumer.
- Custom hook: a function starting with
usethat wraps hook logic for reuse. - Composition: building interfaces by nesting components and passing elements as props.
Summary checklist
- I design components with small props interfaces and clear callback names.
- I use
childrenand slot props instead of many boolean flags. - I forward extra attributes with rest and spread when wrapping native elements.
- I recognise prop drilling and try composition or closer state first.
- I create a context, provide a value, and consume it with
useContext. - I wrap context in a provider component and a guarded custom hook.
- I memoise provider values that would otherwise change on every render.
- I split contexts by concern and update frequency.
- I know that the context default applies only without a provider.
- I remember that context transports state; it does not replace it.
Official sources
- Meta Front-End Developer Professional Certificate (opens in a new tab)
- Course 6: Advanced React (opens in a new tab)
- React: Passing Props to a Component (opens in a new tab)
- React: Passing Data Deeply with Context (opens in a new tab)
- React:
createContextreference (opens in a new tab) - React:
useContextreference (opens in a new tab) - React 19 upgrade guide (opens in a new tab)
