Components, props and JSX
JSX is a function call that builds a plain object, a component is a function from props to those objects, and 0 is the one falsy value React still prints.
After this lesson you can
- Say what a JSX expression evaluates to and where key and children end up
- Explain why props are read-only and how children is just another prop
- Predict what a conditional expression renders, including the 0 trap
A React component is a function that takes one argument, an object of props, and returns a description of what should be on screen. That description is not DOM. It is a tree of plain JavaScript objects called elements, and JSX is the syntax that builds them.
function Greeting({ name, children }: { name: string; children?: React.ReactNode }) {
return <p className="greeting">Hello, {name}. {children}</p>;
}
<Greeting name="Ada" key="g1">Welcome back</Greeting>
The compiler turns each JSX tag into a function call: jsx(type, props, key) from react/jsx-runtime with the modern JSX transform, or
React.createElement(type, attributes, ...children) with the classic
one. Either way very little happens: the attributes are collected into a
props object, whatever sits between the tags goes into
props.children, and key is kept out of the props entirely. The model
below does that job in plain JavaScript, with a createElement-style
signature.
// A cut-down createElement-style jsx(): attributes become props,// children go into props.children, and key is lifted out of props.function jsx(type, config, ...children) { const { key = null, ...props } = config ?? {}; if (children.length === 1) props.children = children[0]; else if (children.length > 1) props.children = children; return { type, key, props };} function Greeting({ name, children }) { return jsx("p", { className: "greeting" }, "Hello, ", name, ". ", children);} export function run() { // <Greeting name="Ada" key="g1">Welcome back</Greeting> const element = jsx(Greeting, { name: "Ada", key: "g1" }, "Welcome back"); return { type: typeof element.type === "function" ? `function ${element.type.name}` : element.type, key: element.key, props: element.props, keyVisibleToComponent: "key" in element.props, // What React would get back when it calls the component: rendered: element.type(element.props), };}Three things follow from that object:
- A capitalised tag is a reference, a lowercase tag is a string.
<Greeting />passes theGreetingfunction astype;<p />passes the string"p". That is why components must start with a capital letter:<greeting />would ask the DOM for a<greeting>tag. childrenis just a prop. A layout component that renders{children}is readingprops.childrenlike any other prop.keyis not a prop. React reads it to identify the element among its siblings, and the component never sees it. If the component needs the id, pass it a second time under another name. (ref, in React 19, is passed to function components as an ordinary prop;forwardRefis no longer needed for that.)
Props are read-only
A component must not modify its props. The parent owns that object and may hand the same one to other children; React also assumes that calling your component with the same props produces the same output. When a component needs a value that changes, that value is state, which the next lessons cover.
What a child expression renders
Anything inside { } in JSX becomes a child, and React has fixed rules for
turning a child into output. , , and
render nothing. Strings and numbers render as text. Arrays render each
item. That makes a convenient conditional, with one sharp
edge: returns its left operand when that operand is falsy, and the
falsy number is a number, so it renders.
// React's rules for turning one child into text.function renderChild(child) { if (child === null || child === undefined || typeof child === "boolean") return ""; if (typeof child === "string" || typeof child === "number") return String(child); if (Array.isArray(child)) return child.map(renderChild).join(""); return "<element>";} export function run() { const items = []; const user = null; const loggedIn = false; return { "loggedIn && 'Welcome'": renderChild(loggedIn && "Welcome"), "user && user.name": renderChild(user && user.name), "items.length && 'You have items'": renderChild(items.length && "You have items"), "items.length > 0 && 'You have items'": renderChild(items.length > 0 && "You have items"), "['a', null, 'b', false]": renderChild(["a", null, "b", false]), };}The items.length && … line is the bug that ships a lone 0 to
production. Write items.length > 0 && …, or use a ternary that returns
null explicitly.
Common mistakes
- Rendering
{count && <Badge />}with a numeric count. When the count is 0 the page shows a literal0, as the second example prints. Compare explicitly:count > 0 && …. - Expecting
props.keyinside the component. It isundefinedthere, because the JSX call removeskeybefore building props. - Mutating a prop "just locally", for example sorting
props.itemsin place. It reorders the parent's array too, and the parent will not re-render to show it. Copy first:[...items].sort(). - Naming a component in lowercase.
<card />is treated as an unknown HTML tag, not yourcardfunction.
What interviewers ask
- What does JSX compile to? — A function call that returns a plain
element object (
type, , ); no DOM is created at that point.
Try it yourself
2 visible tests · 3 hidden testsImplement renderToText(node), returning the text a user would see if
React rendered node (like the textContent of the result).
node is one of:
null,trueorfalse— renders nothing;- a string — renders as itself;
- a number — renders as its decimal text (
0renders"0"); - an array — renders each item in order, concatenated;
- an element
{ "type": "<tag>", "props": { "children": <node> } }— renders itsprops.children(which may be missing, meaning nothing).
Return a single string.
renderToText({"type":"p","props":{"children":["Hello, ","Ada","!"]}})renderToText(["a",false,null,"b",true])
Sign up to run the tests, check the hidden ones and save your progress. Sign up