JavaScript
Arrow functions
- What are arrow functions?
Arrow functions are a shorter syntax for writing functions in JavaScript. They use
=>instead of thefunctionkeyword.// Regular function function add(a, b) { return a + b; } // Arrow function const add = (a, b) => { return a + b; }; // Even shorter (implicit return for single expression) const add = (a, b) => a + b;
- When do you use the short form?
If the function body is a single expression, you can omit the braces and
returnkeyword:// Multiple statements - need braces and return const processUser = (user) => { console.log('Processing:', user.name); return user.id; }; // Single expression - braces optional const getUserId = (user) => user.id; // Single parameter - parentheses optional const double = x => x * 2; // No parameters - parentheses required const getRandom = () => Math.random();
- How do arrow functions work as callbacks?
You often see arrow functions assigned to properties or passed directly. The syntax can be confusing at first.
// This is a property assignment worker.onmessage = (event) => { console.log('FFT complete:', event.data); };
Breaking it down:
worker.onmessageis a property on the worker object=assigns a function to that property(event) => { ... }is the arrow function being assignedWhen the worker sends a message, it calls this function with an
eventparameter
Equivalent regular function syntax:
// Same thing with function keyword worker.onmessage = function(event) { console.log('FFT complete:', event.data); }; // Or defined separately function handleMessage(event) { console.log('FFT complete:', event.data); } worker.onmessage = handleMessage;
All three versions do the same thing: store a function that gets called later when the worker sends a message.
- What about
this? Arrow functions don’t have their own
this- they inherit it from the surrounding code. Regular functions create their ownthis, which can cause confusion in callbacks.function Counter() { this.count = 0; // Regular function - 'this' is undefined in the callback setInterval(function() { this.count++; // Error: 'this' is not the Counter }, 1000); // Arrow function - 'this' refers to Counter setInterval(() => { this.count++; // Works: 'this' is the Counter }, 1000); }
Use arrow functions in callbacks when you need to access the outer
this.
Asynchronous programming
- Why care about asynchronous programming?
JavaScript is single-threaded - it executes one task at a time. Without async programming, slow operations (network requests, file reads) would freeze the entire page. Async programming keeps the page responsive while waiting for operations to complete.
Callbacks
- What is a callback?
A callback is a function passed to another function. The receiving function executes your callback when it finishes its work.
function fetchUser(userId, callback) { setTimeout(function() { const user = { id: userId, name: 'Bob' }; callback(user); // Execute callback with result }, 1000); } fetchUser(123, function(user) { console.log('Got user:', user.name); // Prints after 1 second });
Your callback function is stored and executed later, not immediately. The outer function returns right away, but your callback waits until the async operation completes.
- Why do callbacks lead to “callback hell”?
When operations depend on each other, callbacks nest deeper with each step.
Simple example - getting data in sequence:
// Step 1: Get user fetchUser(123, function(user) { console.log('Got user:', user.name); // Step 2: Need user.id from step 1 fetchPosts(user.id, function(posts) { console.log('Got posts:', posts.length); // Step 3: Need posts[0].id from step 2 fetchComments(posts[0].id, function(comments) { console.log('Got comments:', comments.length); // Each callback nests inside the previous one }); }); });
Why nesting happens: Each step needs data from the previous step. You can’t call
fetchPostsuntil you haveuser.id, and you can’t callfetchCommentsuntil you haveposts[0].id. The only way to access that data is inside the callback where it’s available.With more steps, this becomes unmanageable:
fetchUser(123, function(user) { fetchPosts(user.id, function(posts) { fetchComments(posts[0].id, function(comments) { fetchLikes(comments[0].id, function(likes) { fetchReplies(likes[0].id, function(replies) { console.log('Done!'); // 5 levels deep! }); }); }); }); });
Code shifts rightward, becomes hard to read, and error handling is painful.
Promises
- What is a Promise?
A Promise is a built-in JavaScript object (not a library) that represents a future value. When you start an async operation, you get a Promise immediately - it’s a placeholder that says “I don’t have the result yet, but I will eventually.”
A Promise has three states:
Pending: Operation still running
Fulfilled: Operation succeeded, result is available
Rejected: Operation failed, error is available
- How do you create a function that returns a Promise?
function fetchUser(userId) { return new Promise(function(resolve, reject) { const user = { id: userId, name: 'Bob' }; resolve(user); // Success - call resolve with the result // Or if error: reject(new Error('User not found')) }); }
The Promise constructor takes a function with two parameters:
resolve(value)- Call this when operation succeedsreject(error)- Call this when operation fails
Usage - instead of passing a callback, functions return a Promise:
// Old callback style fetchUser(123, function(user) { console.log(user); }); // Promise style const promise = fetchUser(123); // Returns immediately with a Promise promise.then(function(user) { // Attach callback to Promise console.log(user); });
The key difference: you get a Promise object you can pass around and attach callbacks to later.
- How does
.then()work? Every Promise has a
.then()method. You pass it a function, and.then()returns a new Promise.const promise1 = fetchUser(123); const promise2 = promise1.then(function(user) { console.log(user.name); return 'done'; // This value wraps in a new Promise }); promise2.then(function(value) { console.log(value); // Prints: 'done' });
Key insight: If you return another Promise from
.then(), the next.then()waits for that Promise to resolve:fetchUser(123) .then(function(user) { // Return a Promise - next .then() waits for it return fetchPosts(user.id); }) .then(function(posts) { // Receives posts after fetchPosts resolves console.log(posts); });
- How do Promises help with nesting?
Promises chain with
.then(), avoiding deep nesting:fetchUser(123) .then(function(user) { return fetchPosts(user.id); // Return Promise - next .then() waits }) .then(function(posts) { return fetchComments(posts[0].id); // Return Promise - next .then() waits }) .then(function(comments) { console.log('Comments:', comments.length); }) .catch(function(error) { console.error('Error:', error); // Handles errors from any step });
Each
.then()returns a new Promise, allowing the chain to continue. This flattens the pyramid into a vertical chain.
Async/await
- Why use
async/await? async/awaitis syntax built on top of Promises. It makes Promise-based code read like normal sequential code, eliminating both nested callbacks and.then()chains.- How does it compare?
Fetching user → posts → comments on first post.
Callbacks (nested, hard to read):
fetchUser(123, function(user) { fetchPosts(user.id, function(posts) { fetchComments(posts[0].id, function(comments) { console.log('Comments:', comments.length); }); }); });
Promises (better, but still chaining):
fetchUser(123) .then(user => fetchPosts(user.id)) .then(posts => fetchComments(posts[0].id)) .then(comments => console.log('Comments:', comments.length)) .catch(error => console.error('Error:', error));
This looks shorter, but chaining has problems:
Hard to debug - can’t set breakpoints in arrow functions easily
Hard to use intermediate values - if you need both
userandpostslater, you must restructure the chainNot actually sequential-looking - your eye jumps from
.then()to.then()
async/await (reads top-to-bottom):
async function getUserData() { try { const user = await fetchUser(123); const posts = await fetchPosts(user.id); const comments = await fetchComments(posts[0].id); console.log('Comments:', comments.length); } catch (error) { console.error('Error:', error); } }
Each
awaitpauses execution until the Promise resolves, then continues with the result. No nesting, no chaining.- How do you run operations in parallel?
When operations are independent, use
Promise.allto run them simultaneously:// Sequential (slow): 2s + 2s + 2s = 6 seconds async function loadDataSlow() { const users = await fetch('/users').then(r => r.json()); const posts = await fetch('/posts').then(r => r.json()); const comments = await fetch('/comments').then(r => r.json()); } // Parallel (fast): all start at once = 2 seconds async function loadDataFast() { const [users, posts, comments] = await Promise.all([ fetch('/users').then(r => r.json()), fetch('/posts').then(r => r.json()), fetch('/comments').then(r => r.json()) ]); }
CPU-intensive operations
- Does
async/awaithelp with FFT or image processing? No.
async/awaitonly helps with I/O operations (network, files) where you’re waiting for external resources. CPU computations like FFT still block the main thread.- What are Web Workers?
Web Workers run JavaScript in a separate thread, parallel to your main page. This keeps heavy computations from freezing the UI.
Key points:
Main thread and worker thread run simultaneously (true parallelism)
Workers cannot access the DOM (no
document.getElementById, etc.)Communication happens via
postMessageandonmessageEach worker is a separate JavaScript file
Example - offloading FFT computation:
// Main thread const worker = new Worker('fft-worker.js'); worker.postMessage({ imageData: data }); worker.onmessage = (event) => { console.log('FFT complete:', event.data); };
// fft-worker.js (runs in separate thread) self.onmessage = (event) => { const result = computeFFT(event.data.imageData); self.postMessage(result); // Send result back to main thread };
When to use Web Workers:
async/await: I/O operations (fetch, file read) waiting for external resourcesWeb Workers: CPU-intensive code (FFT, matrix operations, image processing) that blocks execution
In browser JavaScript, you’d handle webhooks by having your backend receive them, then use WebSockets or polling to notify the frontend:
// Frontend connects to your backend via WebSocket const socket = new WebSocket('ws://yourserver.com'); socket.onmessage = (event) => { console.log('Received webhook notification:', event.data); }; // Your backend receives webhook from GitHub, then forwards to frontend via WebSocket
Common syntax
- Destructuring
Extract values from objects or arrays:
const { name, age } = props; const [first, second] = array;
- Optional chaining (
?.) Safely access properties when object might be
nullorundefined:arr?.length // Returns undefined if arr is null/undefined user?.address?.city
- Nullish coalescing (
??) Fallback for
nullorundefined(but not0or''):const max = user.maxPoints ?? 3;
- Logical NOT (
!) Converts to boolean and inverts:
!true // false !0 // true (0 is falsy) !"hello" // false (non-empty strings are truthy)
- Ternary operator
Conditional expression:
const result = condition ? valueIfTrue : valueIfFalse; {isLoggedIn ? <Dashboard /> : <Login />}
- Default parameters
Provide default values for function parameters:
const greet = (name = "stranger") => `Hi ${name}`;
- Shorthand object properties
When variable name matches property name:
const user = { name, age }; // Same as { name: name, age: age }
- Spread operator (
...) Clone or merge arrays/objects:
const newArr = [...oldArr, newItem]; const newObj = { ...oldObj, updated: true };
- Template literals
Multi-line strings and embedded expressions:
const name = "Alice"; const age = 5; const message = `Hello ${name}, you are ${age} years old`; // Multi-line strings (preserves line breaks) const html = ` <div> <h1>${name}</h1> <p>Age: ${age}</p> </div> `;
- Array methods (
map,filter,reduce) Transform arrays without loops:
const numbers = [1, 2, 3, 4, 5]; // map: transform each element const doubled = numbers.map(n => n * 2); // [2, 4, 6, 8, 10] // filter: keep elements that match condition const evens = numbers.filter(n => n % 2 === 0); // [2, 4] // reduce: combine all elements into single value const sum = numbers.reduce((total, n) => total + n, 0); // 15
- Object destructuring with renaming
Extract and rename in one step:
const user = { id: 1, name: "Bob", email: "bob@example.com" }; // Rename 'name' to 'username' during destructuring const { name: username, email } = user; console.log(username); // "Bob"
- Rest parameters (
...) Collect remaining arguments into array:
function sum(...numbers) { return numbers.reduce((total, n) => total + n, 0); } sum(1, 2, 3); // 6 sum(1, 2, 3, 4); // 10 // Also works in destructuring const [first, second, ...rest] = [1, 2, 3, 4, 5]; // first: 1, second: 2, rest: [3, 4, 5]
- Object and array methods
// Object.keys, Object.values, Object.entries const user = { name: "Alice", age: 5 }; Object.keys(user); // ["name", "age"] Object.values(user); // ["Alice", 5] Object.entries(user); // [["name", "Alice"], ["age", 5]] // find, some, every const users = [{id: 1, name: "Alice"}, {id: 2, name: "Bob"}]; users.find(u => u.id === 2); // {id: 2, name: "Bob"} users.some(u => u.id === 2); // true (at least one matches) users.every(u => u.id > 0); // true (all match)
- Optional chaining with function calls
Safely call methods that might not exist:
user.getProfile?.(); // Only calls if getProfile exists data?.items?.[0]?.name // Safely access nested properties and array elements
- Short-circuit evaluation (
&&and||) Conditional execution without if statements:
// && executes right side only if left is truthy isLoggedIn && redirectToDashboard(); // Only redirects if isLoggedIn is true user && user.getName(); // Only calls if user exists // || returns first truthy value (default fallback) const name = user.name || "Guest"; // Use "Guest" if user.name is falsy const config = loadConfig() || getDefaultConfig();
- Array/Object destructuring with defaults
Provide fallback values during destructuring:
const { name = "Unknown", age = 0 } = user; // Use defaults if missing const [first = 1, second = 2] = arr; // Array destructuring with defaults
- Dynamic property names
Compute property names using expressions:
const key = "userName"; const user = { [key]: "Alice", // Property name computed at runtime [`${key}Age`]: 5 }; // Result: { userName: "Alice", userNameAge: 5 }
- Method chaining
Chain operations on arrays:
const result = users .filter(u => u.age > 18) .map(u => u.name) .sort() .slice(0, 10); // Get first 10 sorted names of adults
- String methods
"hello".toUpperCase(); // "HELLO" " hello ".trim(); // "hello" "hello".startsWith("he"); // true "hello".includes("ll"); // true "a,b,c".split(","); // ["a", "b", "c"] ["a", "b", "c"].join(", "); // "a, b, c" "hello".repeat(3); // "hellohellohello"
try/catchfor error handlingHandle errors without crashing:
try { const data = JSON.parse(userInput); processData(data); } catch (error) { console.error("Invalid JSON:", error.message); } // With async/await async function fetchData() { try { const response = await fetch('/api/data'); const data = await response.json(); return data; } catch (error) { console.error("Failed to fetch:", error); return null; } }
setTimeoutandsetIntervalExecute code after delay or repeatedly.
How they work:
setTimeout(function, milliseconds)- Executes function once after delaysetInterval(function, milliseconds)- Executes function repeatedly every X millisecondsBoth return an ID that you can use to cancel them
Time is in milliseconds (1000ms = 1 second)
// setTimeout takes two parameters: // 1. A function (arrow function that logs) // 2. Delay in milliseconds (2000 = 2 seconds) setTimeout(() => { console.log("2 seconds passed"); }, 2000); // With named function function greet() { console.log("Hello!"); } setTimeout(greet, 1000); // Parameter 1: greet function, Parameter 2: 1000ms // setInterval takes the same two parameters // Parameter 1: function to execute, Parameter 2: 1000ms (1 second) const intervalId = setInterval(() => { console.log("Tick"); }, 1000); // Stop the interval using the ID clearInterval(intervalId); // Cancel a timeout before it executes const timeoutId = setTimeout(() => { console.log("This might not run"); }, 5000); clearTimeout(timeoutId); // Cancels it
Common pattern - stop interval after condition:
let count = 0; const intervalId = setInterval(() => { count++; console.log(`Count: ${count}`); if (count >= 5) { clearInterval(intervalId); // Stop after 5 executions console.log("Done!"); } }, 1000); // Runs every second, stops after 5 times
- JSON operations
Parse and stringify JSON data:
const obj = { name: "Alice", age: 5 }; const json = JSON.stringify(obj); // '{"name":"Alice","age":5}' const parsed = JSON.parse(json); // { name: "Alice", age: 5 }
TypeScript
TypeScript adds type checking to JavaScript. Types are checked at compile time, then removed when converted to JavaScript.
- Basic types
const name: string = "Bob"; const age: number = 5; const isActive: boolean = true; const items: number[] = [1, 2, 3]; const data: string[] = ["a", "b", "c"];
- Type aliases
Define custom types:
type Point = { x: number; y: number }; type User = { id: number; name: string; email?: string }; // ? means optional const point: Point = { x: 10, y: 20 }; const user: User = { id: 1, name: "Alice" }; // email is optional
- Function types
Specify parameter and return types:
function add(a: number, b: number): number { return a + b; } const multiply = (a: number, b: number): number => a * b; // Function that returns nothing const logMessage = (msg: string): void => { console.log(msg); };
- Union types
Variable can be one of several types:
type Status = "pending" | "success" | "error"; let status: Status = "pending"; // Can only be one of these three strings type Result = string | number; const value: Result = 42; // Can be string OR number
- Interfaces
Similar to type aliases, commonly used for object shapes:
interface Props { title: string; count: number; onUpdate?: (newCount: number) => void; // Optional callback function } function MyComponent(props: Props) { return <div>{props.title}: {props.count}</div>; }
- Generics
Write flexible, reusable code with type parameters.
What is a generic? A generic is like a placeholder for a type. The function or hook doesn’t know what specific type it will work with until you use it.
React’s `useState` example:
// useState is defined roughly like this: // function useState<T>(initialValue: T): [T, (newValue: T) => void] // You tell useState what type to expect using <Type> const [items, setItems] = useState<string[]>([]); // items: string[] // setItems: (newValue: string[]) => void const [count, setCount] = useState<number>(0); // count: number // setCount: (newValue: number) => void const [user, setUser] = useState<User | null>(null); // user: User | null // setUser: (newValue: User | null) => void
Breaking it down:
useState<string[]>tells TypeScript: “I’m storing an array of strings”TypeScript then knows
itemsisstring[]andsetItemsonly acceptsstring[]useState<number>means storing a number, socountisnumberandsetItemsonly acceptsnumberuseState<User | null>means storing either a User object or null
Writing a generic function:
// <T> is the generic type parameter - a placeholder function getFirst<T>(array: T[]): T | undefined { return array[0]; } // When you call it, specify what T should be const firstNumber = getFirst<number>([1, 2, 3]); // T becomes number const firstString = getFirst<string>(["a", "b"]); // T becomes string
Without generics, you’d need separate functions for each type (
getFirstNumber,getFirstString, etc.). Generics let you write one function that works with any type.
React
Framework design decisions
Real-time manipulation of HTML/CSS in JavaScript is tedious. For interactive front-end applications, React simplifies development with the following design decisions:
Modifying the DOM during rendering is not allowed. The DOM can only be modified after the component is declaratively rendered. React automatically re-renders components when their props or state change.
React wants functions that produce UI components to be pure. Given the same inputs, outputs should always be identical, like a calculator, as long as the function contains only declarative operations such as multiply, add, and divide.
Use
useRefin two ways: (1) to access the DOM after rendering (e.g., focusing an input box or controlling playback), or (2) to store a mutable value that persists across renders but does not trigger a re-render when changed. UsinguseRefdoes NOT cause the component to re-render.
Naming conventions
Use
UpperCasefor component function names (e.g.,MyButton). Unlike Python, dashes are not allowed in JavaScript identifiers.
Tips on useEffect
Use
useEffectto create side effects after a component is rendered. Use event handlers (e.g.,onClick) for side effects based on user actions.useEffectis useful for actions like loading user profiles or syncing with a server, even if the user hasn’t interacted with the component.If you pass an empty array
[]as the dependency list, the effect runs only once after the initial render (componentDidMount).If you include values in the array (e.g.,
[id]), the effect re-runs whenever those values change.
How to avoid expensive recalculations on every render
Use
const myVariable = useMemo(() => { ... }, [values])to store the result of a computation. The value is recalculated only whenvalueschange.useCallbackis similar touseMemobut for functions. It store the function instance, preventing unnecessary re-creations on every render.Other hooks like
useContext,useReducer, anduseLayoutEffectare also available for advanced use cases but I’m not familiar with them atm.
Use good syntax
const limit = Number.isFinite(maxPoints) && maxPoints > 0 ? maxPoints : 3;
isFinite checks whether maxPoints is a valid number (not null, Infinity, etc.). Then, it checks whether it’s greater than 0. If both are true, use maxPoints; otherwise, default to 3.
return next.length <= limit ? next : next.slice(next.length - limit);
Check whether next.length is less than or equal to limit. If yes, return next; otherwise, return the last limit elements. For example, if next is [{a}, {b}, {c}, {d}] and the limit is 3, then next.slice(next.length - limit) returns [{b}, {c}, {d}].
Host via GitHub Pages
Create a new React app using a Vite template:
npm create vite@latest my-app -- --template react
Refer to https://react.dev/learn/build-a-react-app-from-scratch for more.
Install
gh-pagesas a development dependency:npm install --save-dev gh-pages
Update
package.json: add ahomepagefield andpredeploy/deployscripts.{ "name": "my-app", "homepage": "https://<your-username>.github.io/<your-repo-name>/", "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "vite build", "lint": "eslint .", "preview": "vite preview", "predeploy": "npm run build", "deploy": "gh-pages -d dist" }, "dependencies": { } }
Add
base:tovite.config:import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' // https://vite.dev/config/ export default defineConfig({ plugins: [react()], base: '/<your-repo-name>/' })
Run
npm run deploy.
This triggers GitHub Actions and publishes the site to the gh-pages branch. Wait 1 to 2 minutes for the site to appear. Check your repository Settings-Pages to confirm the deployment status. The website will be available on
https://<username>.github.io/<repo-name>/.