Lesson 0.1 · JavaScript in general
Variables, types and operators
const and let, numbers and text, type conversion, === vs. == and useful string methods.
Loading lesson…
Lesson 0.1 · JavaScript in general
const and let, numbers and text, type conversion, === vs. == and useful string methods.
Loading lesson…
People learning React often find that it isn't React that trips them up, but the JavaScript around it. React itself is just a handful of functions – everything else you write in a component is plain JavaScript.
It's like cooking from recipes. Recipes (React) are quick to learn – as long as you can chop, fry and weigh (JavaScript). If you can't chop, every recipe is a struggle. So the first eight lessons are a kind of knife skills course: general JavaScript, no React. The last two lessons of this part then show how these skills are used in React.
A variable is a named box for a value. In modern JavaScript you create one with two keywords:
const name = 'Anna' // const = the box can't be refilledlet count = 0 // let = you can put another value in the box latercount = count + 1 // ✅ let can be reassignedcount += 1 // ✅ the same, shortercount++ // ✅ even shorter (+1)name = 'Peter' // ❌ TypeError: Assignment to constant variable.
| const | let | var | |
|---|---|---|---|
| Can be reassigned? | no | yes | yes |
| Valid in | the block { … } | the block { … } | the whole function (confusing) |
| Use it? | yes, the default choice | when the value changes | no, it's outdated |
A variable only exists inside the curly braces { … } it was created in. Outside, nobody knows about it:
const price = 100if (price > 50) {const discount = 10 // only exists inside this blockconsole.log(price - discount) // 90 – you can see outer variables inside too}console.log(discount) // ❌ ReferenceError: discount is not defined
userName, totalPrice, isLoggedIn.is, has, can, should: isOpen, hasError.users), a single item in the singular (user).getUser, formatPrice, handleClick.Every value in JavaScript has a type. Primitive types are simple values, everything else is an object:
| Type | Examples | typeof returns |
|---|---|---|
| string (text) | 'hello', "hello", `hello` | 'string' |
| number | 42, 3.14, -1, NaN, Infinity | 'number' |
| boolean | true, false | 'boolean' |
| undefined | "no value has been set yet" | 'undefined' |
| null | "intentionally empty" | 'object' ⚠️ a historical bug |
| object | { name: 'Anna' }, [1, 2], new Date() | 'object' |
| function | () => {} | 'function' |
typeof 'hello' // 'string'typeof 42 // 'number'typeof true // 'boolean'typeof undefined // 'undefined'typeof { a: 1 } // 'object'typeof [1, 2] // 'object' – an array is an object too!typeof null // 'object' – a bug from 1995 that never went awayArray.isArray([1, 2]) // true – the right way to recognise an arrayvalue === null // the right way to recognise null
JavaScript has a single type, number, for both whole and decimal numbers.
10 + 3 // 1310 - 3 // 710 * 3 // 3010 / 3 // 3.333333333333333510 % 3 // 1 – the remainder after division (modulo)2 ** 10 // 1024 – powerMath.round(2.5) // 3 – roundsMath.floor(2.9) // 2 – downMath.ceil(2.1) // 3 – upMath.trunc(-2.9) // -2 – cuts off the decimal partMath.abs(-5) // 5Math.min(4, 1, 9) // 1Math.max(4, 1, 9) // 9Math.random() // a random number from 0 (inclusive) to 1 (exclusive)
7 % 2 === 1 // an odd number8 % 2 === 0 // an even numberconst seconds = 187Math.floor(seconds / 60) // 3 – whole minutesseconds % 60 // 7 – the remaining seconds → "3:07"index % 3 // 0, 1, 2, 0, 1, 2… – taking turns in a cycle (colours, say)
0.1 + 0.2 // 0.30000000000000004 😱0.1 + 0.2 === 0.3 // false(0.1 + 0.2).toFixed(2) // '0.30' – CAREFUL, returns TEXTMath.round((0.1 + 0.2) * 100) / 100 // 0.3 – a number rounded to 2 places
A computer stores numbers in binary and can't write 0.1 in it exactly – just as we can't write 1/3 exactly as a decimal. That's why money is often counted in cents (whole numbers) and converted to dollars or euros only when it's displayed.
Number('hello') // NaN0 / 0 // NaNNaN === NaN // false – NaN isn't even equal to itself!Number.isNaN(NaN) // true – the only reliable test10 / 0 // Infinity (not an error)
You can write text in single '…', double "…" or back `…` quotes. Backticks have a superpower: you can put a value inside.
const name = 'Anna'const count = 3'Hello, ' + name + '! You have ' + count + ' messages.' // the old way – easy to forget a space`Hello, ${name}! You have ${count} messages.` // template – ${…} inserts the value`Total: ${count * 12} USD` // any expression can go inside ${…}`Status: ${count > 0 ? 'you have some' : 'empty'}`const html = `<ul><li>A template can span several lines</li></ul>`
Strings can't be changed – every method returns a new text and leaves the original alone.
const text = ' Hello World 'text.length // 15 – the number of characters (spaces included)text.trim() // 'Hello World' – without spaces at the endstext.toUpperCase() // ' HELLO WORLD 'text.toLowerCase() // ' hello world 'const s = 'Hello World's[0] // 'H' – the character at position 0 (counting starts at zero!)s.at(-1) // 'd' – the last characters.includes('World') // trues.startsWith('Hello') // trues.endsWith('!') // falses.indexOf('W') // 6 – where it starts; -1 when not founds.slice(0, 5) // 'Hello' – from position 0 to 5 (5 not included)s.slice(-5) // 'World' – the last 5 characterss.replace('World', 'Anna') // 'Hello Anna' – replaces the FIRST occurrence'a-b-c'.replaceAll('-', '/') // 'a/b/c' – replaces all of them'Anna Smith'.split(' ') // ['Anna', 'Smith'] – splits into an array['a', 'b', 'c'].join(', ') // 'a, b, c' – the opposite: joins an array into text'7'.padStart(2, '0') // '07' – pads on the left to length 2'ha'.repeat(3) // 'hahaha'
'a' < 'b' // true – compares by character codes'Z' < 'a' // true 😕 capital letters have smaller codes'é' > 'f' // true 😕 accented letters come after the English alphabet'é'.localeCompare('f', 'fr') // -1 – "é comes before f" according to French ✅'Anna'.toLowerCase() === 'anna' // comparison that ignores letter case
Everything a user types into a form arrives as text. Even when they type "42". If you want to calculate with it, you have to convert it:
Number('42') // 42Number('3.14') // 3.14Number('3,14') // NaN – a comma isn't a decimal point!Number('') // 0 – careful, empty text is zeroNumber(' 7 ') // 7 – spaces at the ends don't matterNumber('12px') // NaNparseInt('12px') // 12 – reads digits from the left as long as it canparseFloat('3.5kg') // 3.5parseInt('3.9') // 3 – only the whole partString(42) // '42'(42).toString() // '42'42 + '' // '42' – works, but String() is easier to readBoolean('hello') // trueBoolean('') // false!!'hello' // true – a double negation is a shortcut for Boolean()
When JavaScript gets an unexpected type, it tries to convert it on its own. The results can be surprising:
'5' + 3 // '53' – plus with text JOINS texts'5' - 3 // 2 – minus only works with numbers, so it converts the text'5' * '2' // 10true + 1 // 2[] + [] // '''10' > '9' // false 😱 compares texts character by character: '1' < '9'10 > 9 // true
What you see: A text field and, below it, what different conversions make of its contents. This is exactly how values arrive from forms – always as text.
Try it:
text + 1 gives '421' (joined texts), but text * 1 gives the number 42 – multiplication converts the text to a number.Number returns NaN, parseFloat just 3. Only replacing the comma with a dot helps.Number gives up, parseInt reads 12.Number('') is 0, but Boolean('') is false. And '' == 0 is even true.The takeaway: Always convert form text yourself and check it with Number.isNaN. Relying on automatic conversion is a road to bugs.
Loading the interactive part…
Two equals signs (==) convert types before comparing, following complicated rules. Three equals signs (===) compare both the type and the value – no surprises.
| Expression | == (loose) | === (strict) |
|---|---|---|
5 vs '5' | true | false |
0 vs '' | true | false |
0 vs false | true | false |
null vs undefined | true | false |
NaN vs NaN | false | false |
5 === 5 // true5 === '5' // false – a different type5 !== '5' // true – "not equal" in the strict versionage >= 18 // greater than or equalage < 65 && age >= 18 // and at the same time (more in the lesson on conditions)
Two really short functions to warm up: a greeting with a name and double a number.
CHALLENGE: Greeting
1. greet('Anna') should return 'Hello, Anna!'. Use a template string: `Hello, ${name}!`
2. double(4) should return 8.Loading the interactive part…
Write six small functions for working with numbers and text – exactly the kind you need when processing forms. The tests run on their own and show which ones already work.
CHALLENGE: Text helpers (numbers, strings, conversions)
Write the bodies of six small functions. The tests below run on their own and show ✅/❌.
1. initials('Anna Smith') → 'AS'
Split the name at the space (split), take the first letter of both parts ([0]) and make it upper case (toUpperCase).
2. formatTime(187) → '03:07' (seconds → minutes:seconds)
Math.floor(s / 60) is the minutes, s % 60 the seconds. Pad both with a zero on the left to 2 characters (String(…).padStart(2, '0')).
3. parseAmount('12,5') → 12.5, parseAmount('abc') → 0
Many countries write a decimal comma. Replace the comma with a dot (replace) and convert with Number(). If you get NaN (Number.isNaN), return 0.
4. roundTo2(3.14159) → 3.14
Multiply by a hundred, round (Math.round) and divide by a hundred. Return a number, not text!
5. shout(' hello ') → 'HELLO!'
Trim the spaces (trim), make it upper case and add an exclamation mark (template string).
6. isValidEmail('anna@example.com') → true, isValidEmail('anna.example.com') → false
A simple check is enough: it contains '@' (includes) and neither starts nor ends with it (startsWith/endsWith).Loading the interactive part…
const everywhere, let only when you reassign the value, var never. A variable is valid in its block { … }.Array.isArray.0.1 + 0.2). Round with Math.round(x * 100) / 100, toFixed returns text.`…${x}…` instead of joining with plus. String methods return a new text.Number() and watch out for NaN.===, never ==.Checks, Check)Checks, Check – from the file src/course/checks.tsx. The course helper for function-style challenges: runs the tests and shows ✅/❌. You don't need to know how it works inside – it just tells you which steps are done./** One test: a description and a function returning true (passed) or false. It may be async. */export type Check = [name: string, test: () => boolean | Promise<boolean>]/*** Runs the tests of a challenge and shows ✅ or ❌ for each.* An error thrown in a test (e.g. when trying to change a frozen object) is shown under the test.*/export function Checks({ tests }: { tests: Check[] }) {const t = useT()const [results, setResults] = useState<Result[]>(() => tests.map(() => ({ status: 'pending' })))useEffect(() => {let ignore = falsetests.forEach(([, test], i) => {Promise.resolve().then(test).then((ok): Result => (ok ? { status: 'ok' } : { status: 'fail' }),(e): Result => ({ status: 'fail', error: e instanceof Error ? e.message : String(e) }),).then((result) => {if (ignore) returnsetResults((prev) => prev.map((r, idx) => (idx === i ? result : r)))})})return () => {ignore = true}}, [tests])const passed = results.filter((r) => r.status === 'ok').lengthreturn (<div className="stack"><strong className={passed === tests.length ? 'ok' : undefined}>{t.checksPassed(passed, tests.length)}</strong><ul className="list">{tests.map(([name], i) => {const r = results[i] ?? { status: 'pending' }return (<li key={name}>{r.status === 'pending' ? '⏳' : r.status === 'ok' ? '✅' : '❌'} {name}{r.status === 'fail' && r.error && (<div className="bad muted">{t.checksError} {r.error}</div>)}</li>)})}</ul></div>)}
Checks, Check)Checks, Check – from the file src/course/checks.tsx. The course helper for function-style challenges: runs the tests and shows ✅/❌. You don't need to know how it works inside – it just tells you which steps are done./** One test: a description and a function returning true (passed) or false. It may be async. */export type Check = [name: string, test: () => boolean | Promise<boolean>]/*** Runs the tests of a challenge and shows ✅ or ❌ for each.* An error thrown in a test (e.g. when trying to change a frozen object) is shown under the test.*/export function Checks({ tests }: { tests: Check[] }) {const t = useT()const [results, setResults] = useState<Result[]>(() => tests.map(() => ({ status: 'pending' })))useEffect(() => {let ignore = falsetests.forEach(([, test], i) => {Promise.resolve().then(test).then((ok): Result => (ok ? { status: 'ok' } : { status: 'fail' }),(e): Result => ({ status: 'fail', error: e instanceof Error ? e.message : String(e) }),).then((result) => {if (ignore) returnsetResults((prev) => prev.map((r, idx) => (idx === i ? result : r)))})})return () => {ignore = true}}, [tests])const passed = results.filter((r) => r.status === 'ok').lengthreturn (<div className="stack"><strong className={passed === tests.length ? 'ok' : undefined}>{t.checksPassed(passed, tests.length)}</strong><ul className="list">{tests.map(([name], i) => {const r = results[i] ?? { status: 'pending' }return (<li key={name}>{r.status === 'pending' ? '⏳' : r.status === 'ok' ? '✅' : '❌'} {name}{r.status === 'fail' && r.error && (<div className="bad muted">{t.checksError} {r.error}</div>)}</li>)})}</ul></div>)}