React in Depth
Thinking in React: Components, Props and JSX
What React is, how JSX becomes function calls, components as functions, props, and the one-way data flow.
On this page
- Words used in this lesson
- What React is, in one paragraph
- JSX is just function calls
- The rules of JSX
- A component is a function
- Props: the inputs of a component
- Data flows one way: down
- Rendering lists and the key prop
- Conditional rendering
- Keeping components pure
- A mistake that looks like an event handler
- Try it
- Interview questions
- Key takeaways
- What's next
In one line: a React component is a plain JavaScript function that takes an object called props and returns a description of the screen (JSX). React calls your function, reads what it returned, and updates the page to match. If you can write a function, you can write a component.
Most people learn React by copying a counter example, and then something small breaks and nothing makes sense. This lesson goes slowly on purpose. We look at what JSX really turns into, what a component really is, how data moves from parent to child, and what "pure" means. All of it is shown with code that I ran, so you can see the real output and not just trust me.
This is lesson 0 of ten. You do not need to know React. You should know basic JavaScript: functions, objects, arrays and map. The next lesson, State and Rendering, adds the thing that makes a page change over time. Here we only render things once, and pass data around.
What I ran / checked. Everything here ran on Node 22.22.0 with React 19.3.0 and react-dom 19.3.0 (the latest on the npm registry when I wrote this), jsdom 29.1.1 for a fake browser, @testing-library/react 16.3.3, and esbuild 0.28.2 to turn JSX into JavaScript. Server output comes from renderToStaticMarkup in react-dom/server. Click and re-render output comes from createRoot inside jsdom, wrapped in React's act() helper so updates finish before I print. I did not use a bundler dev server. React ran in its development build (the default when NODE_ENV is not set), which is why some checks and warnings appear. I also read these pages from the reactjs/react.dev repository (raw markdown): Describing the UI, Your First Component, Writing Markup with JSX, Passing Props to a Component, Keeping Components Pure, Rendering Lists, and the Strict Mode reference. I wrote everything in my own words. Timings and exact warning wording may differ slightly on your machine and version.
Words used in this lesson
- Component is a function that returns UI. Its name starts with a capital letter, like
Greeting. - JSX is a syntax that looks like HTML but lives inside JavaScript. A build tool turns it into plain function calls.
- Element is a small plain object that says "I want a
<h1>here" or "I want aGreetinghere". It is a description, not the real page. - Props (short for properties) are the inputs you give a component, like
name="Ada". A component reads them. It never changes them. - Children is a special prop. It holds whatever you put between the opening and closing tags of a component.
- Render means React calling your component function to find out what it should show. It does not mean "paint on screen".
- The DOM is the browser's live tree of page elements. React updates it for you.
- Pure means: same inputs always give the same output, and the function changes nothing outside itself while it runs.
- Side effect is anything a function does besides returning a value: changing a variable outside, writing to the page, calling a server.
- Key is a special prop that gives each item in a list a stable identity. We meet it here and use it fully in lesson 1.
- Strict Mode is a development helper that runs your components twice to catch impure code.
What React is, in one paragraph
A web page is a tree of elements in the browser (the DOM). You could change that tree by hand: find a node, set its text, add a child. That works for small pages. In a big app, keeping the tree in sync with your data by hand causes many bugs.
React changes the job. You write a function that says what the page should look like for the current data. When the data changes, React calls your function again, compares the new description with the old one, and changes only the parts of the real page that differ. You describe the result. React does the steps.
That idea has a name: declarative UI. Keep it in mind. Almost every rule in React comes from it.
JSX is just function calls
This looks like HTML inside JavaScript:
function Greeting({ name }) { return <h1 className="big">Hello, {name}!</h1>;}const el = <Greeting name="Ada" />;Browsers cannot run that. A tool (here, esbuild; in a real app, Vite, Next.js or Babel) rewrites it first. I asked esbuild to show me the result:
npx esbuild --jsx=automatic l0/jsx.jsximport { jsx, jsxs } from "react/jsx-runtime";function Greeting({ name }) { return /* @__PURE__ */ jsxs("h1", { className: "big", children: [ "Hello, ", name, "!" ] });}const el = /* @__PURE__ */ jsx(Greeting, { name: "Ada" });console.log(el);So <h1 ...>...</h1> became a call to jsxs("h1", {...}). The first argument is the type (a string for a normal HTML tag, or a function for a component). The second is an object of props. The text between the tags became a prop named children. The old style, still seen in older code, is React.createElement("h1", { className: "big" }, "Hello, ", name, "!"). Both do the same job. (The "automatic" runtime, which is the default in modern setups, is why you no longer need import React at the top of every file.)
What does that call return? I printed el:
{ '$$typeof': Symbol(react.transitional.element), type: [Function: Greeting], key: null, props: { name: 'Ada' }, _owner: null, _store: {}}This is an element. It is just an object. It says: "someone wants a Greeting with props.name equal to 'Ada'". Nothing has been rendered. Greeting has not even been called. Elements are cheap to make and cheap to throw away, and that is why React can afford to create a whole new description on every render.
You never write the $$typeof field. React adds it. It is a marker so React can tell a real element from a random object. (It is also part of how React avoids a class of injection attacks, because JSON from a server can not carry a Symbol.)
The rules of JSX
JSX is stricter than HTML. Six rules cover almost everything.
1. Return one root. A component returns one thing. To return two siblings, wrap them in a fragment, which is an empty tag <>...</> that adds nothing to the page:
function App() { return ( <> <Greeting name="Ada" excited /> <Greeting name="Linus" /> </> );}2. Close every tag. <img />, <input />, <br />. In HTML you may leave them open. In JSX you may not.
3. Use camelCase attribute names. className and not class (because class is a reserved word in JavaScript). htmlFor and not for. onClick and not onclick. The exceptions are data-* and aria-*, which keep their dashes. I checked what comes out:
<div className="a" style={{ backgroundColor: "red", fontSize: 12 }} data-x="1" aria-label="lbl" htmlFor="q" /><div class="a" style="background-color:red;font-size:12px" data-x="1" aria-label="lbl" for="q"></div>Look at style. It takes an object, so there are two sets of braces: the outer pair means "JavaScript goes here", the inner pair is the object. Keys are camelCase, and a plain number like 12 became 12px.
4. Curly braces open a window into JavaScript. Anything inside { } is a normal JavaScript expression: a variable, a function call, a ternary. You cannot put a statement like if or for inside the braces, only expressions.
5. Component names start with a capital letter. This one is a classic trap. React uses the first letter to decide: lowercase means "an HTML tag", uppercase means "a component". I defined a function called greeting (lowercase) and used <greeting name="x" />:
<greeting name="x"></greeting>React did not call my function. It made an unknown HTML element called greeting. Nothing crashed, the browser just shows nothing useful. If your component "does nothing", check the capital letter first.
6. Text is escaped. React escapes strings you put in braces, so a string can not turn into live HTML. I rendered "<script>alert(1)</script>" as a child:
<p><script>alert(1)</script></p>It shows up as harmless text. This is one reason React apps are hard to attack with simple injection, as long as you do not bypass it with dangerouslySetInnerHTML (the name is a warning on purpose).
Now, what can go inside { }? Strings and numbers become text. But true, false, null and undefined render nothing:
<p>[{true}{false}{null}{undefined}]</p><p>[]</p>That is useful, because {isOpen && <Menu />} shows the menu or nothing. But a plain object is an error:
Objects are not valid as a React child (found: object with keys {a}). If you meant to render a collection of children, use an array instead.React does not know how to turn {a: 1} into text. Pick a field, like {user.name}.
A trap with && and the number zero
The "show something or nothing" pattern has one famous bug. 0 is a number, and numbers render. I wrote a component that shows "N new" only when n is truthy:
function Inbox({ n }) { return <div>{n && <span>{n} new</span>}</div>; }With n = 0:
n=0: <div>0</div>A stray 0 appears on the page. The expression 0 && ... gives 0, and React renders the number. The fix is to make the left side a real boolean:
function Inbox2({ n }) { return <div>{n > 0 && <span>{n} new</span>}</div>; }fixed: <div></div>Do not write {count && <X />} when count can be 0. Write {count > 0 && <X />} or use a ternary {count ? <X /> : null}. This shows up in many interviews and in real bug reports.
A component is a function
Here is the same Greeting, with a log line so we can see when it runs:
function Greeting({ name, excited = false }) { console.log("Greeting called with", name); return <h1>Hello, {name}{excited ? "!" : "."}</h1>;}The { name, excited = false } part is destructuring the props object, with a default value for excited. Rendering the App from above to a string:
import { renderToStaticMarkup } from "react-dom/server";console.log(renderToStaticMarkup(<App />));Greeting called with AdaGreeting called with Linus<h1>Hello, Ada!</h1><h1>Hello, Linus.</h1>React called Greeting once for each element, and put the results together. renderToStaticMarkup makes an HTML string on the server. In the browser you use createRoot(...).render(...) and React updates the live DOM, but the calling of your function is the same.
To prove that a component is only a function, I called one by hand:
console.log(Greeting({ name: "Grace" }).type);console.log(Greeting({ name: "Grace" }).props.children);h1[ 'Hello, ', 'Grace', '.' ]It gave back an element object with type: "h1". That is all a component does. (Do not do this in real code. Always let React call components, because hooks, which come in lesson 1, only work when React is the caller.)
Using a component inside another
You build big pages from small components. The App above uses Greeting twice. Each use is independent: two calls, two outputs.
Tips from the official guide that I agree with:
- One component per concern. If a block of JSX has a name in your head ("the profile card"), make it a component.
- Define components at the top level of a file, never inside another component. Lesson 1 shows a real bug this causes, with output.
- You may keep several small components in one file while they are only used there.
Props: the inputs of a component
Props are how a parent talks to a child. They are the arguments of the function, bundled in a single object.
function Card({ title, children }) { return <section><h2>{title}</h2>{children}</section>;}<Card title="T"><p>inside</p></Card><section><h2>T</h2><p>inside</p></section>title is a normal prop. children is special: it is whatever sits between <Card> and </Card>. This pattern lets one component wrap anything, a bit like a picture frame. It is the base of layouts, modals and buttons.
You can pass any JavaScript value as a prop: strings, numbers, arrays, objects and functions. In JSX a string can use quotes (title="T"), and everything else goes in braces (count={3}, user={user}, onSave={save}). A prop with no value, like excited, means excited={true}.
A handy shortcut is the spread: <Avatar {...props} /> passes every prop along. It is short, but it hides what is being passed, so I use it only for thin wrappers.
Props are read-only
This is the first big rule. A component must never change its props. React enforces it in development by freezing the props object. I tried:
function Mut(props) { try { props.name = "changed"; return <i>{props.name}</i>; } catch (e) { return <i>{e.constructor.name}: {e.message}</i>; }}<i>TypeError: Cannot assign to read only property 'name' of object '#<Object>'</i>An assignment to a prop throws a TypeError. Think of props as a snapshot of what the parent gave you for this render. If a component needs to remember something or change something, that is state, and that is the subject of lesson 1.
Data flows one way: down
In React, data goes from parent to child through props. A child can not reach up and change the parent's data. This is called one-way data flow, and it makes bugs easy to trace: to find where a value came from, go up the tree.
How does a child tell the parent "the user clicked"? The parent passes down a function as a prop. The child calls it. Data goes down, events go up. Here is a tiny, real example. Post owns a number. LikeButton only shows it and calls onLike:
function LikeButton({ count, onLike }) { console.log(" LikeButton render, count =", count); return <button onClick={onLike}>Likes: {count}</button>;}function Post() { const [likes, setLikes] = useState(0); console.log("Post render, likes =", likes); return <article><LikeButton count={likes} onLike={() => setLikes(likes + 1)} /></article>;}I mounted it in jsdom, then clicked twice. (useState is the memory hook we cover next lesson. For now read it as "a variable React remembers".)
Post render, likes = 0 LikeButton render, count = 0<article><button>Likes: 0</button></article>Post render, likes = 1 LikeButton render, count = 1<article><button>Likes: 1</button></article>Post render, likes = 2 LikeButton render, count = 2<article><button>Likes: 2</button></article>Follow the story. The parent owns the data. It passes count down and a function onLike down. The child never changes count. On click, the child calls onLike, which changes the parent's state. React then calls Post again, which gives LikeButton a new count. Parent first, then child: that order shows data flows down.
Rendering lists and the key prop
To show an array, use map to turn each item into an element:
const items = ["a", "b"];<ul>{items.map(i => <li key={i}>{i}</li>)}</ul><ul><li>a</li><li>b</li></ul>Notice key={i}. Each element made in a list needs a key, a string or number that is unique among its siblings and stays the same for the same item. React uses keys to know which item is which when the list changes. What happens if you forget? I rendered the list in the browser-like environment without keys:
[console.error] Each child in a list should have a unique "key" prop. ...Check the top-level render call using <ul>.And with a duplicate key (["a", "a"] using the text as key):
[console.error] Encountered two children with the same key, `%s`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted ...Both are warnings in the console, and the page still draws. The real damage shows up later, when you reorder, add or remove items. Lesson 1 shows that with a real input-box bug. For now the rule is:
- Use a stable, unique ID from your data (a database ID) as the key.
- Do not use random values like
Math.random()as keys. A new key on every render means React throws away and rebuilds every item each time. - The array index is only fine when the list never reorders, never gets inserts or deletes in the middle, and the items have no state.
The key is not passed to your component as a prop. It is for React only. If the child needs the ID, pass it again under another name.
Conditional rendering
Components often show different things in different cases. You have three tools, and they are all plain JavaScript:
// 1. early returnif (!user) return <p>Please log in.</p>;// 2. ternary: choose between tworeturn <p>{isOnline ? "Online" : "Offline"}</p>;// 3. && : show or nothing (watch out for 0)return <div>{hasError && <ErrorBox />}</div>;There is nothing React-specific here. If you can write a if or a ternary, you can do conditional rendering. Because null renders nothing, a component can also return null to say "I show nothing right now".
Keeping components pure
Here is the rule that connects everything. A component should be a pure function of its props (and state and context): given the same inputs, it returns the same JSX, and it does not change anything that existed before it started.
Why does React care? Because React decides when to call your function, and it may call it more than once, or call it and then throw the result away. In a future where React pauses rendering to handle a click first, your function could be called and not used. That only works safely if calling it has no side effects.
The classic impure component reads and changes a variable outside itself:
let guest = 0;function Cup() { guest = guest + 1; // changes something outside! return <h2>Guest #{guest}</h2>;}I rendered it twice: once plain, once inside <StrictMode>, and printed the page text:
no StrictMode: <h2>Guest #1</h2>StrictMode : <h2>Guest #2</h2> (counter is now 2)Without Strict Mode, the first render shows "Guest #1". Inside Strict Mode, React deliberately calls the component function twice in development. The counter moved twice, and the page shows #2. The surprise is the clue: pure code does not care how many times you run it. Strict Mode turns "works only if called exactly once" into an obvious bug on day one.
The pure version takes the number as a prop:
function PureCup({ n }) { return <h2>Guest #{n}</h2>; }Give it n={3} a hundred times and you get "Guest #3" a hundred times.
Strict Mode only affects development. It does nothing in your production build. The React docs say all your components in the tree are checked and you can not opt out for a part of the tree below it. New projects from Vite and Next.js turn it on by default. If you see your console.log print twice, this is why. It is not a bug.
What is allowed? Changing things that you created during this render is fine. For example, building a new array with push inside the component, then returning it. The official guide calls this "local mutation". What is not allowed is changing things that already existed: outer variables, props, state, or the DOM. Where do side effects go? Mostly into event handlers (they run when the user does something), and for the rest, into effects, which are lesson 2.
A very common mistake is putting fetch(...), setTimeout(...) or document.title = ... straight in the body of a component. The body runs on every render, possibly twice. Put the work in an event handler or an effect.
A mistake that looks like an event handler
One more frequent beginner bug. Compare these two:
<button onClick={handleClick}>ok</button> // passes the function<button onClick={handleClick()}>bad</button> // calls it right nowThe second one runs handleClick during render, and passes its return value to onClick. I proved it with a counter:
function Oops() { function f() { called++; return () => {}; } return <button onClick={f()}>x</button>;}onClick={f()} calls f during render, before any click: called = 1If you need to pass an argument, wrap it: onClick={() => handleClick(id)}. Lesson 4 covers events in full.
Try it
Do these in a scratch project (npm create vite@latest, choose React) or in any online sandbox that uses React 19.
- Write a component
Badgethat takeslabeland an optionalcolor(default"gray") and renders<span>. Use it three times with different props. - Write
Inboxwith{count && <p>New mail</p>}and render it withcount={0}. What do you see? Fix it. - Render the array
["Ada", "Linus", "Grace"]as a list, first without keys and then with them. What does the browser console say in the first case? - Make a
Framecomponent that takeschildrenand puts a border around them. Wrap an<img>and a paragraph with it. - Write the impure
Cupcomponent above. Wrap your app in<StrictMode>. What number do you see? Then fix it by passing the number in as a prop. - Rename a component to start with a lowercase letter. What changes in the DOM? (Open the browser inspector.)
- In a component, try to set
props.title = "x". What happens?
Answers
- Nothing special:
function Badge({ label, color = "gray" }) { return <span style={{ color }}>{label}</span>; }. - You see a lone
0on the page. Usecount > 0 && ...orcount ? ... : null. - Without keys you get the "unique key prop" warning in the console. With keys it goes away.
function Frame({ children }) { return <div style={{ border: "1px solid" }}>{children}</div>; }. Everything between the tags arrives aschildren.- In Strict Mode (in development) you see a number that moved by two for each use, because the function ran twice. With the prop version it is stable.
- The browser gets an unknown element such as
<badge>in the DOM. Your function is never called. - A
TypeErrorin development, because React freezes props.
Interview questions
What is JSX, and does the browser understand it? (Must know)
JSX is a syntax extension that lets me write markup-like code inside JavaScript. The browser does not understand it. A build step (Babel, esbuild, SWC) turns each tag into a function call, jsx("h1", { children: ... }) in the modern runtime or React.createElement in the older one. That call returns a plain object called an element. So JSX is not a template language. It is JavaScript with a nicer way to write function calls, which is why I can use map, ternaries and variables inside it.
What is the difference between an element and a component? (Must know)
A component is a function (or, in old code, a class) that returns UI. An element is the plain object that describes one thing I want on screen, such as { type: Greeting, props: { name: "Ada" } }. Writing <Greeting name="Ada" /> creates an element. React later calls the Greeting function to find out what to render. Elements are cheap descriptions. Components are the recipes that produce them.
Why must component names start with a capital letter? (Must know)
JSX uses the first letter to decide what the tag means. Lowercase like <div> is a built-in HTML tag, so the type is the string "div". Uppercase like <Card> is a variable that holds a component, so the type is that function. If I name a component card in lowercase, React makes an unknown DOM element <card> and never calls my function. I have seen this cost people an hour, so it is the first thing I check when a component renders nothing.
What are props, and can a component change them? (Must know)
Props are the inputs a parent gives a component, collected in one object. A component can read them but must not change them. React freezes the props object in development, so an assignment throws a TypeError. Props are a snapshot from the parent. If the component needs data that changes over time, it keeps it in state. If the data should change because of something the child did, the child calls a function the parent gave it as a prop.
Explain one-way data flow. (Must know)
Data goes down the tree, from parent to child, as props. A child does not change the parent's data directly. When the child needs to cause a change, the parent passes down a callback prop like onLike, the child calls it, and the parent updates its own state. React then re-renders the parent, and the new value flows down again. This makes it easy to debug: to find who owns a value, I go up the tree until I find where it is created.
What does children mean? (Must know)
It is a special prop that contains whatever JSX I put between the opening and closing tags of a component. A Card that renders {children} inside a frame can wrap anything. It is the main way to build layouts, modals and wrappers without the wrapper needing to know what is inside.
What is a key, and why is it needed in lists? (Must know)
A key is a string or number that identifies a list item among its siblings. React uses it to match items between renders, so it can keep the right state with the right item when the list is reordered, filtered or has items added. Keys should be unique among siblings and stable, ideally an ID from the data. Without them React falls back to position and prints a warning. I would avoid using the index when items can move, and I would never use a random value because it changes every render and remounts every item.
Why does {count && <Badge />} sometimes show a 0? (Trap)
&& returns its left side if that is falsy. When count is 0, the expression gives the number 0, and React renders numbers as text. false, null and undefined render nothing, but 0 does not. I write count > 0 && <Badge /> or a ternary with null. I ran this and the output was <div>0</div>.
What does it mean for a component to be pure, and why does React care? (Senior signal)
A pure component returns the same output for the same props, state and context, and does not change anything that existed before it started rendering. React cares because it controls when and how often it calls my function. In development Strict Mode calls it twice on purpose, and concurrent features can start a render and then throw it away. If my render is pure, none of that matters. If it changes an outside variable, the page shows wrong results, and Strict Mode makes the bug visible. Changing a local array that I created in the same render is fine. Side effects belong in event handlers or effects.
Why does my console.log run twice? (Trap)
Almost always Strict Mode. In development it renders each component twice and, in lesson 2 terms, also runs effects an extra time, to expose impure code. It has no effect in a production build. I do not remove Strict Mode to "fix" the log. I check whether my component is pure, and I make the logging idempotent or remove it.
What is the difference between onClick={handle} and onClick={handle()}? (Trap)
The first passes the function so React can call it later, on click. The second calls the function during render and passes its return value, which is usually undefined. It runs on every render and can cause loops if it sets state. To pass an argument I write onClick={() => handle(id)}.
When is it fine to use the array index as a key? (Senior signal)
When the list is static for the life of the component: it never reorders, no item is inserted or removed except at the end, and items have no state of their own and no inputs. If any of those can happen, the index key makes React reuse the component at that position for a different item, and the wrong state follows the wrong data. I prefer to give data a stable ID when it is created, so I never have to think about this.
Key takeaways
- A component is a function from props to UI. It must start with a capital letter.
- JSX is function calls in disguise.
<X a="1" />becomesjsx(X, { a: "1" }), which returns a plain object called an element. - Props flow down and are read-only. To send information up, pass a function down.
true,false,nullandundefinedrender nothing, but0renders as text. Usen > 0 &&.- In lists, give each item a stable, unique
key, usually an ID from your data. - Keep rendering pure: no changing outside variables, no fetching, no timers in the component body. Strict Mode calls components twice in development to help you notice.
What's next
Next: State and Rendering, lesson 1. Today your components can only show what they are given. Next time they will remember things between renders with useState, and I will show the whole cycle of trigger, render and commit, with a traced example, plus the key bug that makes inputs show the wrong text. Practise now: build a small profile card from three components (Avatar, Name, Card) and pass the data down with props.
Published by Jainik · Last updated · How these lessons are made
Keep learning
Related lessons
- “The API Is Fast but the UI Is Slow”: How to Solve It · Scenario Debugging
- Frontend and React Scenario Bank · Scenario Debugging
- Unnecessary Re-renders: Finding and Fixing Them · Scenario Debugging
Part of the study guide
- React Guide: Components, Hooks, State and Performance
- Next.js Guide: App Router, Server Components and Deploying
Related series