Modern JavaScript Syntax in Components
A lot of stuff you write inside React components has absolutely nothing to do with React.
I think this gets confusing because we learn React and newer JavaScript syntax at almost the same time. You see destructuring, spread, ??, ?., arrow functions, JSX, Hooks, and imports all sitting inside one component file, so naturally everything starts looking like React syntax.
But most of it is just JavaScript.
Take this component:
export default function LessonCard({ lesson, compact = false }) {
const subtitle = lesson.subtitle ?? "No subtitle";
return (
<article className={compact ? "card compact" : "card"}>
<h2>{lesson.title}</h2>
<p>{subtitle}</p>
</article>
);
}There's React stuff in here, sure. We're defining a component and returning JSX which React will use while rendering.
But { lesson, compact = false }? JavaScript.
const? JavaScript.
lesson.subtitle? JavaScript.
??? Also JavaScript.
And this:
compact ? "card compact" : "card"Yep, normal JavaScript conditional expression.
This separation is actually useful when you're stuck. If you don't understand what ?? does, don't go searching React docs for it. Open a JavaScript reference. If you want to know why state gets preserved between renders, now you're asking a React question.
So let's go through the JavaScript syntax you're going to keep seeing in components.
You'll use const a lot
Most local variables inside components are going to use const.
const title = lesson.title;
const completed = lessons.filter((lesson) => lesson.complete);What does const actually mean though?
It means you cannot assign another value to that same binding later.
So this doesn't work:
const title = lesson.title;
title = "Another title";JavaScript will complain because title was declared with const.
But this part catches people: const does not freeze an object.
const lesson = { complete: false };
lesson.complete = true;JavaScript allows this.
Why? Because the lesson binding still points to the same object. We changed a property inside that object, we didn't assign a completely different object to lesson.
This difference becomes pretty important in React because JavaScript allowing a mutation doesn't mean you should mutate React props or state. React code generally treats those values as read-only and creates new values when something changes. We'll get much more into that once we start talking about state updates.
Use let when the variable itself really does need another value.
let label;
if (complete) {
label = "Complete";
} else {
label = "In progress";
}Nothing wrong with let there.
And var?
You'll still see it in older JavaScript, but I wouldn't use it in new component code. const and let are block scoped and usually much easier to reason about when you're reading a function.
Object destructuring
Props come into a component as one object.
So this:
function LessonCard(props) {
return <h2>{props.title}</h2>;
}is completely valid.
If props contains title and minutes, you can read them through props.title and props.minutes.
But you'll very commonly see this instead:
function LessonCard({ title, minutes }) {
return <h2>{title}, {minutes} min</h2>;
}That { title, minutes } syntax is object destructuring.
JavaScript reads the title property from the incoming object and creates a local variable named title. Same for minutes.
React isn't doing the destructuring. React passes the props object to your function, and then JavaScript handles the function parameter syntax.
Also, object destructuring works by property name, not position. So both of these pass the same props:
<LessonCard title="JSX" minutes={12} />
<LessonCard minutes={12} title="JSX" />Order doesn't mean anything here because the properties are still named title and minutes.
You can also read one property and give the local variable another name.
const { title: lessonTitle } = lesson;This one looks a bit strange the first time.
The object still has a property called title. JavaScript reads that property and stores its value in a local variable called lessonTitle.
And if you also use TypeScript, don't confuse that colon with a type annotation. Inside an object destructuring pattern, this colon is renaming the local binding.
Defaults with destructuring
You can put defaults directly inside the pattern too.
function LessonCard({ minutes = 10 }) {
return <p>{minutes} min</p>;
}If minutes comes in as undefined, JavaScript uses 10.
That includes the case where the prop wasn't passed at all:
<LessonCard />But the default does not replace every empty-looking value.
If minutes is null, you get null.
If it's 0, you get 0.
If it's false, you get false.
If it's an empty string, you get an empty string.
Only undefined activates that destructuring default.
That's worth remembering because sometimes people expect this:
function LessonCard({ minutes = 10 }) {}to mean "use 10 whenever minutes doesn't contain a useful value."
It doesn't mean that. JavaScript only checks for undefined there.
You can destructure nested objects too:
const {
author: { name },
} = lesson;This reads lesson.author.name into a local variable called name.
Personally, once data can be missing, I wouldn't get too clever with nested destructuring because this throws if lesson.author is undefined.
Something like this is usually easier to understand:
const authorName = lesson.author?.name ?? "Unknown author";Now absence is handled right there in the expression instead of relying on every nested object existing.
Array destructuring
Objects destructure by property name. Arrays destructure by position.
const coordinates = [12, 24];
const [x, y] = coordinates;x gets the item at position 0, and y gets the item at position 1.
You've already seen this syntax if you've seen useState:
const [count, setCount] = useState(0);The square bracket syntax there does not belong to React.
useState() returns values in an array, and JavaScript destructuring puts the first one into count and the second one into setCount.
React decides what those two array entries contain. JavaScript handles pulling them out.
You can skip positions too:
const [, secondLesson] = lessons;Now we ignore position zero and read position one.
That's legal, although once you start skipping three or four positions, your code gets pretty annoying to read. At that point I'd usually just use an index or give the values proper names earlier.
Rest syntax collects what's left
You're going to see ... doing a few different jobs in JavaScript, which is mildly annoying because the punctuation looks identical.
Look at this component:
function TextField({ label, id, ...inputProps }) {
return (
<label htmlFor={id}>
{label}
<input id={id} {...inputProps} />
</label>
);
}Inside the function parameter, ...inputProps is rest syntax.
JavaScript takes label and id out first, then puts the remaining own enumerable properties into a new object called inputProps.
So if somebody renders:
<TextField
label="Email"
id="email"
name="email"
required
autoComplete="email"
/>then label and id get their own local variables, while name, required, and autoComplete end up inside inputProps.
Then this part:
<input id={id} {...inputProps} />uses spread syntax to pass those properties onto the input.
Same ..., different job depending on where it appears.
One syntax rule here: rest has to come last.
This is invalid:
const { ...rest, title } = lesson;You select the named properties first, then collect whatever remains.
Object spread
Now let's use ... inside an object literal.
const nextLesson = {
...lesson,
complete: true,
};Here JavaScript creates a new object and copies lesson's own enumerable properties into it. Then it writes complete: true.
Order is important because if the same property appears more than once, the later value wins.
So:
const nextLesson = {
...lesson,
complete: true,
};will always end with complete set to true.
But flip the order:
const nextLesson = {
complete: true,
...lesson,
};and now lesson.complete gets copied afterwards.
If lesson.complete was false, then nextLesson.complete ends up false.
This is one of those tiny bits of syntax that can create a very stupid bug. Read object spread from top to bottom. If one property should override the copied value, put that property after the spread.
When an object contains the same property more than once, the later value wins.
Object spread only copies one level
This one is very important when we start updating React state.
Suppose we've got:
const lesson = {
title: "Modern JavaScript",
author: {
name: "Ishtmeet",
},
};And then:
const nextLesson = { ...lesson };We created a new outer object.
But JavaScript did not recursively copy author.
So lesson.author and nextLesson.author still point to the same nested object.
You can verify that:
console.log(lesson === nextLesson);
// false
console.log(lesson.author === nextLesson.author);
// trueNow if you do this:
nextLesson.author.name = "Another name";you've also changed:
lesson.author.namebecause both outer objects still contain the same author object.
If you need to change something inside author without modifying the old data, copy that level too:
const nextLesson = {
...lesson,
author: {
...lesson.author,
name: "Another name",
},
};Now the outer lesson object is new and the nested author object is new.
Any other nested objects you didn't copy are still shared.
This comes up constantly with React state, so don't read { ...something } as "deep copy this object." JavaScript did not deep copy it. It only copied that outer level.
Array spread works in a similar way
You can create a new array using spread:
const nextLessons = [...lessons, newLesson];The old lessons array stays as it was, and nextLessons gets a new array containing the old items plus newLesson at the end.
Want it at the start?
const nextLessons = [newLesson, ...lessons];Easy enough.
But again, don't assume all the objects inside got copied.
If lessons contains object references, the new array contains those same object references.
const nextLessons = [...lessons];
console.log(nextLessons === lessons);
// false
console.log(nextLessons[0] === lessons[0]);
// trueNew array, same item object at position zero.
So copying an array and then mutating one of its existing item objects can still modify data referenced by the old array.
We'll use these copying patterns a lot when state comes in, because React state updates usually mean creating new arrays or objects instead of changing the existing ones.
Optional chaining with ?.
Optional chaining is that ?. syntax you keep seeing around object reads.
const authorName = lesson.author?.name;Normally, if you try:
lesson.author.nameand author is undefined, JavaScript throws because you're asking for .name from something that doesn't exist.
With:
lesson.author?.nameJavaScript checks author first. If it's null or undefined, the expression gives you undefined instead of continuing to .name.
If author exists, then .name gets read normally.
You can use it at multiple levels:
const city = lesson.author?.address?.city;Maybe author can be missing. Maybe address can also be missing. Both cases are allowed here.
But pay attention to exactly where you put ?..
const city = lesson.author?.address.city;If author is missing, the optional chain stops and you get undefined.
But if author exists and address is undefined, then .city still throws.
So don't just randomly add one ?. somewhere near the start and assume the whole expression became safe. Put it at the points where null or undefined are actually allowed.
And optional chaining only checks for null and undefined. It doesn't validate your data.
If author.name exists but contains the number 42 when your code expected a string, ?. has nothing to say about that.
?? and || are not interchangeable
Another syntax you'll see everywhere is nullish coalescing:
const authorName = lesson.author?.name ?? "Unknown author";?? uses the value on the right only when the left side is null or undefined.
So if:
lesson.author?.namegives undefined, we use "Unknown author".
Now compare that with ||.
const countWithOr = count || "Not counted";
const countWithNullish = count ?? "Not counted";Suppose:
count = 0;With ||, zero is falsy, so you get:
Not countedWith ??, zero isn't null or undefined, so you keep:
0Same difference comes up with false and "".
So which one should you use?
Depends on what your data means.
If zero is a perfectly valid value and you only want a fallback when data is missing, ?? probably fits.
If an empty string should also count as "nothing useful here", then || may be exactly what you want.
Don't pick one because it looks newer. Pick based on which values your application considers valid.
Template strings
Backticks create template strings:
const label = `Lesson ${current} of ${total}`;Anything inside ${...} is a JavaScript expression.
So if:
current = 2;
total = 10;then label becomes:
Lesson 2 of 10Pretty straightforward.
They come up all the time when building URLs too:
const href = `/learn/${chapter.slug}/${lesson.slug}`;And you can put the whole JavaScript expression directly inside JSX:
<a href={`/learn/${lesson.slug}`}>{lesson.title}</a>There are two syntaxes sitting beside each other there.
The {...} belongs to JSX and says "evaluate JavaScript here."
The backticks and ${...} belong to JavaScript's template string syntax.
Once you know which parser syntax you're looking at, this stuff becomes much less weird.
One thing with URLs though: template strings don't make a URL safe. If a value can come from an untrusted source, you still need to validate what URLs or schemes your application allows.
Computed property names
Sometimes the name of an object property is itself stored in a variable.
Say:
const fieldName = "weeklyGoal";Now you want to create an object using the value inside fieldName as the property name.
You do this:
const nextForm = {
...form,
[fieldName]: "3",
};Those square brackets tell JavaScript to evaluate fieldName.
So the resulting object contains:
{
weeklyGoal: "3"
}Without the brackets:
const nextForm = {
fieldName: "3",
};you literally get a property named fieldName.
This shows up quite a bit in forms because one event handler can read an input's name and update that same property in your form object.
For a tiny form with two fields, writing separate updates may still be easier to read. You don't have to turn everything into generic code just because JavaScript lets you.
Returning objects from arrow functions
There's one annoying little arrow-function syntax rule worth knowing.
Suppose you're mapping over lessons and want to return a new object:
const nextLessons = lessons.map((lesson) => (
{ ...lesson, selected: false }
));The parentheses around the object are there because a bare {} immediately after the arrow can be read as the function body.
You can also write it with return:
const nextLessons = lessons.map((lesson) => {
return {
...lesson,
selected: false,
};
});Both versions return an object.
I usually use the short form when the object is small and obvious, and switch to the block form when there's enough going on that I want proper intermediate variables.
Trying to make every map() callback fit into one line isn't really an achievement.
import and export are JavaScript too
React projects use modules everywhere:
import LessonCard from "./LessonCard.jsx";
export default function LessonList() {}import and export are JavaScript module syntax.
React doesn't provide them.
And these braces:
import { LessonCard } from "./lesson-ui.jsx";have nothing to do with props destructuring, even though they look similar.
Here we're asking the JavaScript module system for a named export called LessonCard.
Compare that with:
const { LessonCard } = ui;Now we're destructuring a runtime object.
Same braces, different part of JavaScript grammar.
You'll see this a lot while learning frontend code: punctuation gets reused. Read what surrounds it before deciding what the syntax means.
Don't turn every expression into one giant expression
Newer JavaScript syntax lets us pack quite a lot into one line.
For example:
return <p>{lesson.author?.name?.trim().toUpperCase() ?? "UNKNOWN"}</p>;Sure, it works for certain data.
But now you've mixed missing-author handling, missing-name handling, trimming, casing, and fallback behavior into one expression.
If there's some actual application rule hidden in there, I'd rather name the steps.
const rawName = lesson.author?.name;
const trimmedName = rawName?.trim();
const displayName = trimmedName || "Unknown author";
return <p>{displayName}</p>;Now you can actually see the decision.
We're deliberately using || for displayName because an empty string after trimming should also get the fallback. If we used ??, an empty string would stay an empty string.
That's the kind of detail which gets buried when you try to compress everything.
Shorter code isn't automatically easier code.
Sometimes one expression is perfect. Sometimes three boring variables are much easier to understand six months later.
This is standardized JavaScript
Destructuring, rest, spread, optional chaining, nullish coalescing, template strings, computed property names... all of this is JavaScript syntax.
You don't need React for any of it.
You can open Node or a browser console and use these features without importing a single React package.
What you do need to check sometimes is what JavaScript version your target browsers or runtime support.
Build tools can transform some newer syntax into older syntax, but that doesn't mean every newer JavaScript API suddenly exists in an older browser. Syntax transformation and runtime API support are two different problems.
So if you're using something newer, especially an API rather than just syntax, check what your actual runtime supports and what your build setup is doing with it.
Know which layer you're looking at
When you read a React component, try separating the JavaScript work from the React work.
Take this:
function LessonCard({ lesson }) {
const title = lesson.title ?? "Untitled";
return <h2>{title}</h2>;
}JavaScript destructures lesson from the props object. JavaScript reads lesson.title. JavaScript runs the ?? expression and stores the result in title.
Then that value appears inside JSX, and React uses the returned element description while rendering the component.
Same with:
const [count, setCount] = useState(0);React's part is useState(0) and what that Hook does.
The [count, setCount] part is plain JavaScript array destructuring.
And:
const nextLesson = {
...lesson,
complete: true,
};has no React syntax in it at all. That's just JavaScript creating an object.
React may care about why you're creating a new object instead of mutating state, but the syntax doing the copy still comes from JavaScript.
Once you start separating these pieces, component code becomes a lot easier to read. You stop treating every unfamiliar character inside a .jsx file as some React feature you forgot to learn.
Sometimes the React question is actually just a JavaScript question.
Quite often, actually.