.tsx fileIf/else, loops, arrow functions with ( ) or { }, props, keys, onClick and state updates: everything we used in the Dev Stack project, with live examples. The React demos run the real React 19.2.8 library, the same production build page 2 checks against.
A component is a normal JavaScript function. Before return we are in plain JavaScript; inside return ( … ) we are in JSX. Almost every “how do I write X?” question is answered by asking: which zone am I in?
| Meaning | Examples | |
|---|---|---|
| Expression | Gives back a value. It could stand on the right of const x = … | 5, tech.name, a + b, a ? b : c, ok && <p/>, list.map(…), getName() |
| Statement | Does something; gives no value | if (…) {…}, for (…) {…}, while, const x = 1;, return |
Test: can we write const result = ___ with it? Yes → expression → allowed inside JSX's { }. No → statement → only in Zone 1.
// 1. Imports (top of the file) import { useState } from "react"; import type { Technology } from "../types"; // 2. Things outside components: run ONCE, when the file loads const BADGE_COLORS: Record<string, string> = { Frontend: "bg-sky-50 text-sky-700" }; // 3. The props type: what this component expects to be given type TechCardProps = { prop_tech: Technology; prop_added: boolean; }; // 4. The component function TechCard({ prop_tech, prop_added }: TechCardProps) { // ── Zone 1: normal JavaScript ── const badgeColor = BADGE_COLORS[prop_tech.category] ?? "bg-gray-100"; // ── Zone 2: JSX ── return ( <article> <h3>{prop_tech.name}</h3> </article> ); } // 5. Export, so other files can import it export default TechCard;
return)Everything works exactly as in plain JavaScript. Use this zone to prepare values, then just use them in the JSX.
function Example({ stack }: { stack: Technology[] }) {
// variables
const count = stack.length;
let label = "";
// if / else if / else
if (count === 0) {
label = "No technologies selected yet.";
} else if (count === 1) {
label = "1 Technology Selected";
} else {
label = count + " Technologies Selected";
}
// switch
switch (count) {
case 0: console.log("empty"); break;
default: console.log("not empty");
}
// loops
for (let i = 0; i < stack.length; i++) console.log(stack[i].name);
for (const tech of stack) console.log(tech.name);
stack.forEach((tech) => console.log(tech.name));
// a helper function
function isAdded(id: string) {
return stack.some((t) => t.id === id);
}
return <p>{label}</p>; // Zone 2 only USES what Zone 1 prepared
}
function Panel({ stack }: { stack: Technology[] }) {
if (stack.length === 0) {
return <p>Your stack is empty.</p>; // stop here
}
return <ul>…</ul>; // only reached when not empty
}
return null; means “draw nothing”.
return){ }<h3>{prop_tech.name}</h3>
<p>{stack.length} selected</p>
<p>{prop_tech.rating * 2}</p> {/* any expression, maths too */}
<img src={prop_tech.icon} alt={prop_tech.name} /> {/* attribute: braces, NO quotes */}
<span className="text-xs">fixed text</span> {/* plain text: quotes, no braces */}
src="{prop_tech.icon}" with quotes gives the literal text {prop_tech.icon}. Quotes or braces, never both.
condition ? A : B{prop_added ? "✓ Added to Stack" : "Add to Stack"}
{stack.length === 0 ? (
<div>Your stack is empty.</div>
) : (
<ul>…</ul>
)}
Read it as “true? → A, otherwise → B”. The round brackets around multi-line JSX are only for neatness.
condition && A{stack.length > 0 && <button>Remove all</button>}
Trap: {stack.length && <button/>} prints a literal 0 on the page when the stack is empty, because 0 && … is 0 and React prints numbers. Make the left side a real true/false: stack.length > 0 && ….
{n && <b>has items</b>} show?{count === 0 ? "None" : count === 1 ? "One" : "Many"} // works, but hard to read
Better: decide in Zone 1 (the label example in Section 3) and write {label} in the JSX.
.map()for is a statement, so it can't go in { }. .map() is an expression: it returns a new array, here an array of JSX, and React draws every item in it.
<ul>
{stack.map((tech) => (
<li key={tech.id}>{tech.name}</li>
))}
</ul>
// filter first, then map
{technologies
.filter((t) => t.category === "Frontend")
.map((t) => <TechCard key={t.id} prop_tech={t} … />)}
| HTML | JSX | Why |
|---|---|---|
class="…" | className="…" | class is a reserved word in JavaScript |
for="email" (label) | htmlFor="email" | for is reserved too |
onclick="…" | onClick={() => …} | camelCase, and the value is a function |
<img>, <br> | <img />, <br /> | every tag must be closed |
style="color:red" | style={{ color: "red" }} | outer braces = JavaScript, inner braces = an object |
<!-- comment --> | {/* comment */} | a JS comment inside braces |
| several top-level elements | one parent: <div>…</div> or <>…</> | return gives back only one thing; the fragment <> adds no element |
onClick={() => { … }} is allowed in Zone 2Zone 2 only allows expressions, and a function written as an arrow is an expression: its value is a function. It passes the test, because const handle = () => { … } is valid. So it may go inside { }.
Writing the function does not run it. React stores it on the button and calls it on click. And inside the function's own { } body we are inside a function again, so Zone 1 rules apply there: const, if, console.log and return are all fine.
<button
onClick={() => { // Zone 2: a function VALUE (an expression) ✓
console.log("clicked", prop_tech.name); // inside the body: Zone 1 again, statements ✓
if (prop_added) return;
prop_onAdd(prop_tech);
}}
>
Add to Stack
</button>
Inside onClick={ } we write | What the button receives | Runs |
|---|---|---|
() => prop_onAdd(prop_tech) | a small function (short body, automatic return) | on click ✓ |
() => { …several statements… } | a small function (block body) | on click ✓ |
handleClick (defined in Zone 1, no brackets) | that function | on click ✓ |
handleClick() or prop_onAdd(prop_tech) | the result of calling it now (undefined) | immediately, while drawing ✗ |
When the handler grows, move it to Zone 1 and pass it by name. It reads more cleanly and does the same thing:
function TechCard({ prop_tech, prop_onAdd, prop_added }: TechCardProps) {
// Zone 1: a normal function
function handleClick() {
if (prop_added) return;
prop_onAdd(prop_tech);
}
return (
<button onClick={handleClick}>Add to Stack</button> // the NAME, no ()
);
}
Sticky note: a zone is about where code sits. JSX's { } takes expressions, and a function is an expression. Once we are inside a function's { } body, we are back in normal JavaScript, so a JSX attribute can hold a whole little Zone 1.
( ) vs { }const double = (n) => n * 2;
const card = (t) => (
<li>{t.name}</li>
);const double = (n) => {
const result = n * 2;
return result;
};| We write | Returns |
|---|---|
(t) => t.name | t.name ✓ |
(t) => (<li>{t.name}</li>) | the <li> ✓ |
(t) => { return <li>{t.name}</li>; } | the <li> ✓ |
(t) => { <li>{t.name}</li> } | undefined ✗: braces but no return, so .map() gives an array of nothing and no cards appear, with no error |
() => { name: "React" } | undefined ✗: JS reads the braces as a block. Write () => ({ name: "React" }) to return an object |
["React", "Vue", "Svelte"].map(…) give back? (runs real JavaScript)Choose ( ) when the function is just one value or one piece of JSX. Choose { } + return when we need statements first (a const, an if, a console.log).
// ( ): just JSX {stack.map((tech) => ( <li key={tech.id}>{tech.name}</li> ))} // { }: a statement first, so return is needed {stack.map((tech) => { const short = tech.name.slice(0, 3); return <li key={tech.id}>{short}</li>; })}
{ } run now, not on clickThe browser does not understand JSX. Before the page runs, Vite turns every tag into a function call that builds a plain object:
<button onClick={prop_onAdd(prop_tech)}>Add to Stack</button>
// becomes roughly
jsx("button", { onClick: prop_onAdd(prop_tech), children: "Add to Stack" })
JavaScript works out every value of an object immediately, like const s = { total: add(2, 3) } runs add right away. So onClick={prop_onAdd(prop_tech)} calls it while drawing and stores its result (undefined) as the click handler.
| We write | When it runs | With what | Result |
|---|---|---|---|
A onClick={() => prop_onAdd(prop_tech)} | on click | the technology | ✓ correct |
B onClick={prop_onAdd(prop_tech)} | immediately, while drawing | the technology | ✗ every card adds itself on page load; the button does nothing |
C onClick={prop_onAdd} | on click | the click event | ✗ the wrong thing is added |
Sticky note: onClick needs a function, not the result of calling one. To pass an argument, wrap the call: () => doIt(x). Same rule as useEffect(fn): fn hands the function over, fn() calls it now.
Props work exactly like function arguments.
| Plain JavaScript | React |
|---|---|
Define: function greet(name) { … } | Define: function TechCard({ prop_tech }) { … } |
Call: greet("Aksan") | Use: <TechCard prop_tech={tech} /> |
name is the argument | prop_tech is a prop |
<TechCard prop_tech={tech} />
// └───┬───┘ └─┬─┘
// prop NAME prop VALUE (together: one prop)
// React collects every prop into ONE object, the "props":
TechCard({ prop_tech: reactObject })
| Word | Means |
|---|---|
| prop | one name + value pair, for example prop_tech={tech} |
| prop name | prop_tech, the label the child looks for |
| prop value | tech, what is sent |
| props | the object holding all of a component's props |
// Technologies.tsx (sending) <TechCard key={tech.id} techs={tech} /> // TechCard.tsx (receiving): the same label in 3 places type TechCardProps = { techs: Technology }; function TechCard({ techs }: TechCardProps) { return <h3>{techs.name}</h3>; }
When two siblings (the cards and the Your Stack panel) need the same data, it lives in their common parent. The parent passes the data down to show it, and a function down so children can ask for changes.
Technologies owns stack + addToStack
│ <TechGrid onAdd={addToStack} stack={stack} />
▼
TechGrid passes them on
│ <TechCard prop_onAdd={onAdd} prop_added={stack.some(…)} … />
▼
TechCard button: onClick={() => prop_onAdd(prop_tech)}
// addToStack, onAdd and prop_onAdd are the SAME function
Sticky note: functions go down so children can ask; data comes down so children can show it. Props are read-only: a child never changes its props, it calls a function the parent gave it.
key: a name tag for list itemsWhen a list re-renders, React compares the new items with the old ones. With a key it matches by name tag (“the item tagged vue is still vue”); without one it matches by position, which goes wrong when items are removed or reordered.
| Rule | Why |
|---|---|
Put key on the element .map() returns | That is the list React compares |
Unique within that list, and stable for the same item (tech.id) | The same technology always gets the same tag |
| Avoid the array index when items can be added, removed or reordered | The bug in the right-hand list above |
key is not a prop | React keeps it; the component never receives it |
When we call setStack(newValue), React checks Object.is(oldValue, newValue). For objects and arrays that is an identity check (“is it the very same object in memory?”), not a check of the contents.
| What we pass to setStack | Object.is(old, new) | Redraw? |
|---|---|---|
the same array after stack.push(tech) | true (same object) | No ✗ |
[...stack, tech] | false (new object) | Yes ✓ |
stack.filter((t) => t.id !== id) | false (filter always makes a new array) | Yes ✓ |
[] (remove all) | false | Yes ✓ |
Sticky note: never change state in place (push, pop, splice, stack[0] = …). Add with [...stack, x], remove with stack.filter(…), clear with [].
Sections 7 to 9 are the pieces. This section is the order to put them in. Every feature of the Dev Stack page (add, “✓ Added”, remove, remove all) was built with the same five steps, always in this order.
| Step | Question to ask | In our code |
|---|---|---|
| 1 · Owner | Which component should remember this data? The closest component that contains everyone who reads or changes it. | The cards change stack, the panel shows it, so Technologies (their common parent) owns it |
| 2 · Memory | Create the state, in the owner | const [stack, setStack] = useState<Technology[]>([]); |
| 3 · Changer | Write the functions that change it, next to the state (only the owner has the setter) | addToStack, removeFromStack, clearStack |
| 4 · Hops | Pass data or changers down, one component at a time, to where they are needed | Technologies → TechGrid → TechCard |
| 5 · Use | Show the data, or call the changer on a click | the panel's list, the button's onClick |
| Edit | Where | Example (TechGrid → TechCard) |
|---|---|---|
| send | the parent's JSX | <TechCard prop_added={…} /> |
| declare | the child's type …Props | prop_added: boolean; |
| receive | the child's parameter list | function TechCard({ …, prop_added }) |
If TypeScript underlines a prop in red, one of the three edits is missing: usually “declare”.
| Feature | Owner · Memory | Changer | Hops | Use |
|---|---|---|---|---|
| A · Add to the stack | new: Technologies, useState | new: addToStack | 2 hops carrying a function: onAdd → prop_onAdd | button onClick; panel list |
| B · “✓ Added” and the count | already there | none | 2 hops carrying data: stack → prop_added (one true/false per card) | button disabled, colour, text; count line |
| C · Remove one, remove all | already there | new: removeFromStack (filter), clearStack ([]) | none: the buttons are in the panel, which is inside the owner | ✕ on each row; “Remove all” |
In feature C we wrote no code to turn a removed card's button back to “Add to Stack”. It happens anyway: prop_added is not saved anywhere, it is worked out from stack on every draw (stack.some(…) in TechGrid). Change the owner's state, and everything worked out from it updates by itself.
Sticky note: one piece of state, in one owner. If a value can be worked out from state (a count, “is it added?”, a filtered list), work it out while drawing; do not make a second useState for it, or the two will drift apart.
Most of the shape of the code can be decided from the design, before any code exists. Three short lists, on paper:
| Plan | How | What it decides |
|---|---|---|
| 1 · Boxes | Draw a box around each part of the design and give it a noun. A part becomes its own component if it has its own heading, repeats (cards, rows), has its own buttons or behaviour, or appears on more than one page. | which components and files exist, and the tree (who is inside whom) |
| 2 · Data | For every value that changes, write who shows it and who changes it. | the owner (step 1 above) and every hop, so you know the props before writing them |
| 3 · Looks | For every box, list the different ways it can look: loading, empty, filled, error, selected… | where conditions go. 2 looks → ? :. 1 look or nothing → &&. 3+ looks, or a look more than a few lines long → a variable in Zone 1, or its own component |
| Box | Why it is a component | Looks |
|---|---|---|
Navbar, Banner, Footer | own part of the page | Navbar: menu open / closed on phones |
Technologies | own heading; owns stack | – |
TechGrid | waits for the data (Suspense) | loading / loaded |
TechCard | repeats 12 times | added / not added |
StackPanel | own heading, own buttons | empty / list |
| Changing value | Shown by | Changed by | Owner |
|---|---|---|---|
| the 12 technologies | TechGrid | nobody (loaded once) | module level + use() in TechGrid |
stack | TechCard (“added?”), StackPanel | TechCard (add), StackPanel (✕, remove all) | Technologies, their common parent |
| menu open? | Navbar | Navbar | Navbar itself |
Then write a skeleton first: every component returning a placeholder (<div>StackPanel</div>), wired together with its props, and check the layout in the browser. Fill each box in afterwards. The plan will not be perfect, and moving a block into its own component later (“refactoring”) is a normal part of the job, but the plan gets the big shape right first.
| Syntax | Meaning | From our project |
|---|---|---|
: type | “this is of this type” | function addToStack(tech: Technology) |
type X = { … } | describe an object's shape | type TechCardProps = { prop_tech: Technology; … } |
boolean, string, number | basic types | prop_added: boolean |
Technology[] | an array of Technology | stack: Technology[] |
(tech: Technology) => void | a function that takes a Technology and returns nothing | prop_onAdd |
useState<Technology[]>([]) | the state's type in < >; without it, [] would be never[] | stack |
Record<string, string> | an object with string keys and string values | BADGE_COLORS |
Promise<Technology[]> | a promise that will give a Technology array | loadTechnologies() |
import type { … } | import only a type (removed from the final JavaScript) | import type { Technology } |
a ?? b | a, unless a is null or undefined, then b | BADGE_COLORS[…] ?? "bg-gray-100" |
`text ${x}` | template literal: a string with JavaScript inside ${ } | className={`… ${badgeColor}`} |
| We want to… | Zone 1 (before return) | Zone 2 (inside JSX { }) |
|---|---|---|
| Show a value | – | {tech.name} |
| if / else | if (…) {…} else {…} | {cond ? A : B} |
| if, no else | if (…) {…} | {cond && A} (cond must be true/false) |
| else if | if … else if … else into a variable | {label} |
| Loop | for, for…of, forEach | {list.map((x) => <li key={x.id}>…</li>)} |
| Filter a list | const fe = list.filter(…) | {list.filter(…).map(…)} |
| Make a variable | const x = … | ✗ not allowed; make it in Zone 1 |
| Show something else entirely | if (…) return <p>…</p>; | – |
| Comment | // … | {/* … */} |
| Run on click | – | onClick={() => doIt(x)} |
| Pass data to a child | – | <Child name={value} />, received as function Child({ name }) |
| Build a feature | Owner → Memory → Changer → Hops (send, declare, receive) → Use (section 10) | |
| Update an array state | setList([...list, x]), setList(list.filter(…)) | – |