← From a JSON file to the screen
Web dev track · companion to page 2 · TSX syntax

How to write code in a .tsx file

If/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.

1 · The one rule: a component has two zones

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?

function TechCard({ prop_tech, prop_added }: TechCardProps) { Zone 1 · normal JavaScript / TypeScript (before return) const, let, if / else, switch, for, for…of, while, functions, console.log const badgeColor = BADGE_COLORS[prop_tech.category] ?? "bg-gray-100"; if (!prop_tech) return null; Zone 2 · JSX (inside return ( … )) HTML-like tags; JavaScript only inside { }, and only EXPRESSIONS ✓ {prop_tech.name} ✓ {a ? b : c} ✓ {ok && <p/>} ✓ {list.map(…)} ✗ {if (…) …} ✗ {for (…) …} ✗ {const x = 1} }

Expression or statement?

MeaningExamples
ExpressionGives 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()
StatementDoes something; gives no valueif (…) {…}, 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.

Try it: expression or statement?

Pick an answer for each line.

2 · The file skeleton

// 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;

3 · Zone 1: normal JavaScript (before 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
}

Early return: “if X, show something else entirely”

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”.

4 · Zone 2: JSX (inside return)

4.1 Show a value: { }

<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.

4.2 if / else → 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.

4.3 “if” with no else → 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 && ….

Try it with real React: what does {n && <b>has items</b>} show?

{n && …}
loading React…
{n > 0 && …}
loading React…

4.4 else if (three or more cases)

{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.

4.5 Loops → .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} … />)}

4.6 JSX is not quite HTML

HTMLJSXWhy
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 elementsone parent: <div>…</div> or <>…</>return gives back only one thing; the fragment <> adds no element

4.7 Functions inside JSX: why onClick={() => { … }} is allowed in Zone 2

Zone 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 writeWhat the button receivesRuns
() => 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 functionon 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.

5 · Arrow functions: ( ) vs { }

Body with ( ) or nothing: automatic return
const double = (n) => n * 2;
const card = (t) => (
  <li>{t.name}</li>
);
Body with { }: you MUST write return
const double = (n) => {
  const result = n * 2;
  return result;
};
We writeReturns
(t) => t.namet.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

Try it: what does ["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>;
})}

6 · The braces { } run now, not on click

The 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 writeWhen it runsWith whatResult
A onClick={() => prop_onAdd(prop_tech)}on clickthe technology✓ correct
B onClick={prop_onAdd(prop_tech)}immediately, while drawingthe technology✗ every card adds itself on page load; the button does nothing
C onClick={prop_onAdd}on clickthe click event✗ the wrong thing is added

Try it with real React: look at the log BEFORE clicking

loading React…

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.

7 · Props: how a parent hands data to a child

Props work exactly like function arguments.

Plain JavaScriptReact
Define: function greet(name) { … }Define: function TechCard({ prop_tech }) { … }
Call: greet("Aksan")Use: <TechCard prop_tech={tech} />
name is the argumentprop_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 })
WordMeans
propone name + value pair, for example prop_tech={tech}
prop nameprop_tech, the label the child looks for
prop valuetech, what is sent
propsthe object holding all of a component's props

The name does not have to match the value, but both sides must agree

// 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>;
}

Passing a function down: “lifting state up”

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.

8 · key: a name tag for list items

When 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.

Try it with real React: tick React in both lists, then remove the first item

key={tech.id}
loading React…
key={index}
loading React…
Tick the box next to React in BOTH lists, then remove the first item.
RuleWhy
Put key on the element .map() returnsThat 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 reorderedThe bug in the right-hand list above
key is not a propReact keeps it; the component never receives it

9 · State updates: always a new array or object

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 setStackObject.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)falseYes ✓

Try it with real React: push vs a new array

loading React…

Object.is in real JavaScript

Sticky note: never change state in place (push, pop, splice, stack[0] = …). Add with [...stack, x], remove with stack.filter(…), clear with [].

10 · Building a feature: the five-step order

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.

StepQuestion to askIn our code
1 · OwnerWhich 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 · MemoryCreate the state, in the ownerconst [stack, setStack] = useState<Technology[]>([]);
3 · ChangerWrite the functions that change it, next to the state (only the owner has the setter)addToStack, removeFromStack, clearStack
4 · HopsPass data or changers down, one component at a time, to where they are neededTechnologies → TechGrid → TechCard
5 · UseShow the data, or call the changer on a clickthe panel's list, the button's onClick

Every hop is three edits

EditWhereExample (TechGrid → TechCard)
sendthe parent's JSX<TechCard prop_added={…} />
declarethe child's type …Propsprop_added: boolean;
receivethe child's parameter listfunction TechCard({ …, prop_added })

If TypeScript underlines a prop in red, one of the three edits is missing: usually “declare”.

The three features, step by step

FeatureOwner · MemoryChangerHopsUse
A · Add to the stacknew: Technologies, useStatenew: addToStack2 hops carrying a function: onAdd → prop_onAddbutton onClick; panel list
B · “✓ Added” and the countalready therenone2 hops carrying data: stack → prop_added (one true/false per card)button disabled, colour, text; count line
C · Remove one, remove allalready therenew: removeFromStack (filter), clearStack ([])none: the buttons are in the panel, which is inside the owner✕ on each row; “Remove all”

Worked out, not stored

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.

Before you write: plan the boxes, the data and the looks

Most of the shape of the code can be decided from the design, before any code exists. Three short lists, on paper:

PlanHowWhat it decides
1 · BoxesDraw 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 · DataFor 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 · LooksFor 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

The plan for the Dev Stack page

BoxWhy it is a componentLooks
Navbar, Banner, Footerown part of the pageNavbar: menu open / closed on phones
Technologiesown heading; owns stack–
TechGridwaits for the data (Suspense)loading / loaded
TechCardrepeats 12 timesadded / not added
StackPanelown heading, own buttonsempty / list
Changing valueShown byChanged byOwner
the 12 technologiesTechGridnobody (loaded once)module level + use() in TechGrid
stackTechCard (“added?”), StackPanelTechCard (add), StackPanel (✕, remove all)Technologies, their common parent
menu open?NavbarNavbarNavbar 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.

11 · The TypeScript parts we used

SyntaxMeaningFrom our project
: type“this is of this type”function addToStack(tech: Technology)
type X = { … }describe an object's shapetype TechCardProps = { prop_tech: Technology; … }
boolean, string, numberbasic typesprop_added: boolean
Technology[]an array of Technologystack: Technology[]
(tech: Technology) => voida function that takes a Technology and returns nothingprop_onAdd
useState<Technology[]>([])the state's type in < >; without it, [] would be never[]stack
Record<string, string>an object with string keys and string valuesBADGE_COLORS
Promise<Technology[]>a promise that will give a Technology arrayloadTechnologies()
import type { … }import only a type (removed from the final JavaScript)import type { Technology }
a ?? ba, unless a is null or undefined, then bBADGE_COLORS[…] ?? "bg-gray-100"
`text ${x}`template literal: a string with JavaScript inside ${ }className={`… ${badgeColor}`}

12 · Cheat sheet

We want to…Zone 1 (before return)Zone 2 (inside JSX { })
Show a value–{tech.name}
if / elseif (…) {…} else {…}{cond ? A : B}
if, no elseif (…) {…}{cond && A} (cond must be true/false)
else ifif … else if … else into a variable{label}
Loopfor, for…of, forEach{list.map((x) => <li key={x.id}>…</li>)}
Filter a listconst fe = list.filter(…){list.filter(…).map(…)}
Make a variableconst x = …✗ not allowed; make it in Zone 1
Show something else entirelyif (…) 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 featureOwner → Memory → Changer → Hops (send, declare, receive) → Use (section 10)
Update an array statesetList([...list, x]), setList(list.filter(…))–

13 · Quick quiz