JavaScript

Arrow functions

What are arrow functions?

Arrow functions are a shorter syntax for writing functions in JavaScript. They use => instead of the function keyword.

// 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 return keyword:

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

  1. worker.onmessage is a property on the worker object

  2. = assigns a function to that property

  3. (event) => { ... } is the arrow function being assigned

  4. When the worker sends a message, it calls this function with an event parameter

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 own this, 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 fetchPosts until you have user.id, and you can’t call fetchComments until you have posts[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 succeeds

  • reject(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/await is 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 user and posts later, you must restructure the chain

  • Not 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 await pauses 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.all to 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/await help with FFT or image processing?

No. async/await only 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 postMessage and onmessage

  • Each 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 resources

  • Web 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 null or undefined:

arr?.length      // Returns undefined if arr is null/undefined
user?.address?.city
Nullish coalescing (??)

Fallback for null or undefined (but not 0 or ''):

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/catch for error handling

Handle 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;
  }
}
setTimeout and setInterval

Execute code after delay or repeatedly.

How they work:

  • setTimeout(function, milliseconds) - Executes function once after delay

  • setInterval(function, milliseconds) - Executes function repeatedly every X milliseconds

  • Both 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 items is string[] and setItems only accepts string[]

  • useState<number> means storing a number, so count is number and setItems only accepts number

  • useState<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 useRef in 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. Using useRef does NOT cause the component to re-render.

Naming conventions

  • Use UpperCase for component function names (e.g., MyButton). Unlike Python, dashes are not allowed in JavaScript identifiers.

Tips on useEffect

  • Use useEffect to create side effects after a component is rendered. Use event handlers (e.g., onClick) for side effects based on user actions. useEffect is 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 when values change.

  • useCallback is similar to useMemo but for functions. It store the function instance, preventing unnecessary re-creations on every render.

  • Other hooks like useContext, useReducer, and useLayoutEffect are 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

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

  2. Install gh-pages as a development dependency:

    npm install --save-dev gh-pages
    
  3. Update package.json: add a homepage field and predeploy/deploy scripts.

    {
      "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": {
      }
    }
    
  4. Add base: to vite.config:

    import { defineConfig } from 'vite'
    import react from '@vitejs/plugin-react'
    
    // https://vite.dev/config/
    export default defineConfig({
      plugins: [react()],
      base: '/<your-repo-name>/'
    })
    
  5. 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>/.