A component is a function that returns markup. That is the entire idea, and everything else in React is built on it.
function Greeting() {
return <h1>Hello</h1>;
}You then use it as if it were an HTML tag:
<Greeting />That is the whole trick. HTML gives you a fixed vocabulary of tags; components let you add your own, and yours can take arguments, hold state and contain other components.
A real one
Here is the footer of the pizza app — a component with no props, no state, and nothing clever about it, which is what most components look like:
import { Container } from 'react-bootstrap';
export function Footer() {
return (
<footer className="bg-pizza-black text-white-50 mt-5 py-4">
<Container>
<div className="d-flex flex-wrap justify-content-between gap-2 small">
<span>PizzaHub — a demo app for lovemesomecoding.com</span>
<span>Not a real restaurant. Please do not expect a pizza.</span>
</div>
</Container>
</footer>
);
}Note <Container> with a capital C sitting next to <footer>
with a lowercase f. Both are used the same way. One is a component imported from a library, the
other is a real HTML element — and the capital letter is how React tells them apart.
The rules
The name must start with a capital letter
This is not style, it is syntax. JSX compiles <footer> to the string
'footer' and <Footer> to the variable Footer. Name your
component footer and React will look for an HTML element called footer,
find your props meaningless, and render nothing useful — with no error.
It must return one thing
A function returns one value, and a component is a function. Two sibling elements need a wrapper. When you do not want a real element in the output, use a fragment:
// Won't compile — two roots.
return (
<h1>Menu</h1>
<p>Choose a pizza</p>
);
// Fragment: groups without adding a DOM node.
return (
<>
<h1>Menu</h1>
<p>Choose a pizza</p>
</>
);The guarded-route component ends with exactly this, because it must return the children it was given without wrapping them in a stray div:
return <>{children}</>;Declare components at the top level, never inside another component
This is the one that produces a genuinely baffling bug:
// WRONG.
function MenuPage() {
const [filter, setFilter] = useState('ALL');
// A brand-new function on every render of MenuPage.
function ProductRow({ product }) {
return <li>{product.name}</li>;
}
return <ul>{products.map((p) => <ProductRow key={p.id} product={p} />)}</ul>;
}Because ProductRow is redefined every render, React sees a different component
type each time and cannot match the old tree to the new one. It destroys every row and rebuilds
it. Any state inside those rows is lost, any input loses focus mid-typing, and every effect re-runs.
It looks like a state bug and it is a definition-location bug.
Move it out to the top level and pass what it needs as props.
Keep it pure
Given the same props and state, a component must return the same markup and change nothing outside itself while doing so.
let renderCount = 0;
function ProductCard({ product }) {
renderCount++; // WRONG — a side effect during render
return <div>{product.name}</div>;
}React reserves the right to call your component whenever it likes, more than once, and to throw
the result away. StrictMode deliberately renders twice in development to surface exactly
this — see Rendering to the DOM. Anything that reaches outside
the component belongs in an event handler or an effect, not in the body.
Calculating during render is fine, and good. This runs on every render and is entirely correct, because it only reads:
const cheapest = Math.min(...product.sizes.map((s) => s.price));
const isPizza = product.type === 'PIZZA';Importing and exporting
One component per file is the usual convention, named after the file. This project uses named exports:
// src/components/Footer.tsx
export function Footer() { /* … */ }
// src/App.tsx
import { Footer } from './components/Footer';A file can export several — CartContext.tsx exports both
CartProvider and the useCart hook, because they are two halves of one
thing and splitting them would help nobody.
Composition
Components nest, and the nesting is the application. Here is the pizza app's root, stripped of its routes:
export default function App() {
const [cartOpen, setCartOpen] = useState(false);
return (
<div className="d-flex flex-column min-vh-100">
<AppNavbar onOpenCart={() => setCartOpen(true)} />
<main className="flex-grow-1">
<ErrorBoundary>
<Suspense fallback={<Spinner />}>
<Routes>{/* … */}</Routes>
</Suspense>
</ErrorBoundary>
</main>
<CartDrawer show={cartOpen} onHide={() => setCartOpen(false)} />
<Footer />
</div>
);
}Read that top to bottom and you have the layout of the site. Every capitalised tag is a file you can open. This is what people mean when they say React scales: the shape of the code is the shape of the screen.
Class components
You will meet these in any codebase older than about 2020, and in a great many tutorials that have not been updated. Same component, both ways:
// The modern way.
function Greeting({ name }: { name: string }) {
return <h1>Hello {name}</h1>;
}
// The class way — equivalent, and no longer how new code is written.
class Greeting extends React.Component<{ name: string }> {
render() {
return <h1>Hello {this.props.name}</h1>;
}
}Classes are not deprecated and will not be removed, but hooks made them unnecessary: function
components can now hold state, run effects and do everything a class could, with less ceremony and
no this to bind.
One exception survives. There is still no hook equivalent of
componentDidCatch, so an error boundary must be a class — even in a codebase that is
otherwise entirely modern. This app has exactly one class component for that reason, and
Error Boundaries is about it.
Next
JSX — the markup-looking syntax that is not actually markup.