Function Components
A function component is a plain JavaScript function that returns JSX describing what should appear on the screen. It is the standard way to build UI in modern React: instead of manually creating and updating DOM elements, you describe the desired result for a given set of inputs (props and state), and React figures out how to update the real DOM to match. Function components, combined with hooks, are how virtually all React code is written today — class components still exist but are considered legacy.
Overview / How it works
At its core, a function component is nothing more than a JavaScript function that accepts a single argument — the props object — and returns a React element tree (written using JSX). React calls this function whenever it needs to know what the component should render. That call is called a render. A render does not immediately touch the DOM; it produces a lightweight description of the UI (the Virtual DOM). React then compares this new description to the previous one in a process called reconciliation, calculates the minimal set of changes needed, and applies only those changes to the real DOM in the commit phase. This is why React is described as declarative: you describe what the UI should look like for the current data, not the step-by-step instructions to mutate the DOM.
Because a function component is just a function, by itself it cannot remember anything between calls — every render starts from scratch, with fresh local variables. To let a component have memory (like a counter value, an input’s current text, or whether a modal is open) React provides hooks, special functions like useState and useEffect that hook into React’s internal per-component storage. When state managed by a hook changes, React schedules a re-render: it calls the function component again, hooks return the updated values, and JSX is recomputed. This is the fundamental link between state and UI updates — you never manipulate the DOM directly, you just update state and let React re-render.
Hooks rely on being called in the exact same order on every render, because React tracks each hook’s state by call order internally, not by name. This is why hooks must only be called at the top level of a component (never inside loops, conditions, or nested functions) — more on this in Common Mistakes.
Syntax
A function component is typically written as an arrow function (or a function declaration) whose name starts with a capital letter, and which returns JSX:
function Greeting(props) {
return <h1>Hello, {props.name}!</h1>;
}
export default Greeting;
| Part | Purpose |
|---|---|
function Greeting(props) |
A plain JS function. Capitalized name tells React (and JSX) this is a component, not an HTML tag. |
props |
A single object argument containing all attributes passed by the parent, e.g. <Greeting name=\"Ana\" /> gives props.name === \"Ana\". |
return <h1>...</h1> |
Must return a single root element (or a Fragment) describing the UI, or null to render nothing. |
export default |
Makes the component importable from other files, e.g. import Greeting from \"./Greeting\"; |
An equivalent arrow-function form, very common in modern codebases:
const Greeting = (props) => {
return <h1>Hello, {props.name}!</h1>;
};
export default Greeting;
Props are usually destructured directly in the parameter list for readability: const Greeting = ({ name }) => <h1>Hello, {name}!</h1>;
Examples
Example 1: A simple prop-driven component
function Welcome({ name }) {
return <p>Welcome back, {name}.</p>;
}
function App() {
return (
<div>
<Welcome name="Priya" />
<Welcome name="Diego" />
</div>
);
}
export default App;
Renders: two paragraphs, “Welcome back, Priya.” and “Welcome back, Diego.”
This shows the essence of components: the same Welcome function is reused twice with different props, producing different output each time. Props flow one-way, from parent to child — Welcome never modifies name, it only reads it.
Example 2: A function component with local state
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
function handleIncrement() {
setCount(count + 1);
}
return (
<div>
<p>Count: {count}</p>
<button onClick={handleIncrement}>Add One</button>
</div>
);
}
export default Counter;
Renders: a paragraph reading “Count: 0” and a button labeled “Add One”. Clicking the button re-renders the component with “Count: 1”, then “Count: 2”, and so on.
useState(0) gives this component its own private piece of memory, initialized to 0. It returns a pair: the current value (count) and a setter function (setCount). Calling setCount tells React the state has changed, which schedules a re-render — React calls Counter again, useState returns the new count, and the JSX reflects it. This is the closed loop between state and rendering that drives all interactivity in React.
Example 3: A function component with an effect (fetching data)
import { useState, useEffect } from "react";
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(`https://api.example.com/users/${userId}`)
.then((res) => res.json())
.then((data) => {
if (!cancelled) {
setUser(data);
setLoading(false);
}
});
return () => {
cancelled = true;
};
}, [userId]);
if (loading) return <p>Loading...</p>;
return <p>{user.name}'s profile</p>;
}
export default UserProfile;
Renders: “Loading…” immediately, then (once the fetch resolves) a paragraph like “Priya’s profile”.
This demonstrates a function component managing two pieces of state (user and loading) and a side effect (fetching data) via useEffect. The dependency array [userId] tells React to re-run the effect only when userId changes, not on every render. The cleanup function (the function returned from the effect) guards against calling setUser after the component has unmounted or userId has changed again mid-fetch, preventing a stale response from overwriting a newer one.
How it works step by step / Under the hood
- On mount: React calls the function component for the first time. Hooks initialize their state (e.g.
useStatesets its initial value;useEffectcallbacks are recorded but not run yet). React builds the element tree, commits it to the real DOM, and only then runs anyuseEffectcallbacks whose dependencies apply. - On a state update: Calling a setter function (like
setCount) does not change the DOM directly — it schedules a re-render. React re-invokes the function component from top to bottom. Hooks are matched to their previous state by the order they were called in, which is why the order must never change between renders. React then diffs the new returned JSX against the previous render’s output (reconciliation) and applies only the minimal DOM changes (commit). AnyuseEffectwhose dependency array changed re-runs its cleanup function first, then its new effect. - On unmount: When a component is removed from the tree (e.g. its parent stops rendering it), React runs the cleanup function of every active effect one last time, then discards the component’s hook state entirely.
Common Mistakes
Mistake 1: Calling a hook conditionally
function Profile({ isLoggedIn }) {
if (isLoggedIn) {
const [name, setName] = useState("");
}
return <p>Profile</p>;
}
This breaks the Rules of Hooks. React tracks hook state purely by call order, not by name, so if useState only runs on some renders, every hook after it gets misaligned with the wrong stored state on the next render, causing subtle bugs or crashes.
Fix: always call the hook unconditionally at the top level, and put the condition inside the logic that uses the value:
function Profile({ isLoggedIn }) {
const [name, setName] = useState("");
if (!isLoggedIn) return <p>Please log in</p>;
return <p>Profile: {name}</p>;
}
Mistake 2: Mutating state directly
function TodoList() {
const [items, setItems] = useState(["Buy milk"]);
function addItem(text) {
items.push(text);
setItems(items);
}
return (
<ul>
{items.map((item) => (
<li>{item}</li>
))}
</ul>
);
}
Mutating the existing array with push and passing the same reference back to setItems does not reliably trigger a re-render, because React compares state by reference to decide whether anything changed. It also skips a key prop on the list items, which React needs to track each item across renders.
Fix: create a new array and give each item a stable key:
function TodoList() {
const [items, setItems] = useState(["Buy milk"]);
function addItem(text) {
setItems([...items, text]);
}
return (
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
);
}
Best Practices
- Name components with a capital letter (
Greeting, notgreeting) so JSX and React can distinguish them from HTML tags. - Keep components small and focused on one responsibility; compose larger UIs from smaller ones.
- Destructure props in the function signature for readability:
function Card({ title, onSelect }). - Never mutate state or props — always create new arrays/objects when updating.
- Always call hooks unconditionally at the top level, in the same order every render.
- Give every list item a stable, unique
key(usually an id, not the array index, unless the list never reorders). - Keep the
useEffectdependency array honest — include every reactive value the effect reads. - Prefer pure rendering logic: given the same props and state, a component should always return the same JSX.
Practice Exercises
- Write a function component
Temperaturethat accepts acelsiusprop and renders both the Celsius value and its Fahrenheit conversion. - Build a
LikeButtonfunction component with its ownlikedboolean state (viauseState) that toggles between “Like” and “Liked” text when clicked. - Create a
Clockcomponent that usesuseStateanduseEffect(withsetIntervaland a cleanup function) to display the current time, updating every second.
Summary
- A function component is a JavaScript function, named with a capital letter, that takes
propsand returns JSX. - React calls the function to render, diffs the result against the previous render (reconciliation), and commits only the necessary DOM changes.
- Hooks like
useStateanduseEffectgive function components memory and side effects; they must always run in the same order on every render. - Updating state schedules a re-render; state and props must always be treated as immutable.
- Effects run after the DOM commits, and their cleanup functions run before the next effect and on unmount.
