Skip to main content

Frontend engineer coding interview questions

21 questions on coding for frontend engineer candidates. Expect prompts such as “Implement a debounce function in TypeScript” and “Write a useFetch hook that handles loading, error, and abort”, each with a worked answer outline and the follow-ups interviewers push on.

Questions

Showing 1 to 21 of 21 coding questions.

As asked

Write a generic debounce function in TypeScript that delays execution of a callback until a specified number of milliseconds have passed since the last call. It should return a function with the same signature as the input and a cancel method.

Sample answer outline

Should return a typed wrapper that clears and resets a timer on each call. The cancel method clears the pending timer. Strong answers add proper generic typing so the returned function infers parameter and return types from the input function, and mention that the return value is undefined if debounced (not yet fired).

Reference implementation (typescript)

TypeScript
function debounce<T extends (...args: unknown[]) => unknown>(
  fn: T,
  delay: number
): ((...args: Parameters<T>) => void) & { cancel: () => void } {
  // implement here
}

Expect these follow-ups

  • How would you add a leading option so the first call fires immediately and trailing calls are debounced?
  • How does this differ from throttle and when would you use each in a search input?
typescriptalgorithmsclosurestimers

As asked

Implement a useFetch<T>(url: string) hook that returns { data, loading, error }. It should cancel the inflight request when the component unmounts or the URL changes, and it should not set state after unmount.

Sample answer outline

Uses AbortController in the useEffect body, passing signal to fetch(). The cleanup function calls controller.abort(). Catches AbortError and does not set error state for it. Strong answers also handle race conditions when URL changes rapidly (the abort handles this), and type the hook correctly with a generic.

Reference implementation (typescript)

TypeScript
import { useState, useEffect } from 'react';

function useFetch<T>(url: string): {
  data: T | null;
  loading: boolean;
  error: Error | null;
} {
  // implement here
  return { data: null, loading: true, error: null };
}

Expect these follow-ups

  • How would you add a retry mechanism with exponential backoff?
  • When would you replace this custom hook with React Query or SWR and what do you gain?
reacthookstypescriptasyncfetch

As asked

Given a string, find the first character that does not repeat anywhere in the string. If no such character exists, return an empty character or null. Optimize for a single pass through the string.

Sample answer outline

Two-pass approach: first pass builds a frequency hashmap, second pass scans left to right and returns the first character with count 1. A true single-pass approach uses an ordered map or a list of (char, first_index) pairs updated as you scan. A strong candidate mentions character set assumptions (ASCII vs Unicode), handles uppercase vs lowercase, and asks whether the empty string should raise or return a sentinel.

Reference implementation (python)

Python
def first_unique_char(s: str) -> str:
    count = {}
    for ch in s:
        count[ch] = count.get(ch, 0) + 1
    for ch in s:
        if count[ch] == 1:
            return ch
    return ''

Expect these follow-ups

  • What if the string is too large to fit in memory and arrives as a stream?
  • Extend this to find the first character that appears exactly K times.
company:adobehashmapstringcodingfrequency-count

As asked

Airbnb's listing availability service caches per-listing calendar data to avoid hitting the database on every search request. Implement an LRU (Least Recently Used) cache that supports get and put in O(1) time. Then explain how you would extend it to be thread-safe in a Node.js or Java service.

Sample answer outline

The canonical solution uses a doubly linked list for O(1) removal combined with a hash map for O(1) lookup. The candidate should walk through get (move node to head), put (insert at head, evict tail if at capacity), and explain pointer bookkeeping. For thread safety they should discuss synchronizing the composite operation, or using a read-write lock.

Reference implementation (python)

Python
class LRUCache:
    def __init__(self, capacity: int):
        self.cap = capacity
        self.cache = {}  # key -> node
        # sentinel head and tail
        self.head, self.tail = Node(0, 0), Node(0, 0)
        self.head.next = self.tail
        self.tail.prev = self.head

    def get(self, key: int) -> int:
        if key not in self.cache:
            return -1
        self._move_to_head(self.cache[key])
        return self.cache[key].val

    def put(self, key: int, value: int) -> None:
        if key in self.cache:
            node = self.cache[key]
            node.val = value
            self._move_to_head(node)
        else:
            node = Node(key, value)
            self.cache[key] = node
            self._add_to_head(node)
            if len(self.cache) > self.cap:
                tail = self._remove_tail()
                del self.cache[tail.key]

Expect these follow-ups

  • What changes if you need the cache to be distributed across multiple servers?
  • How would you add a TTL expiry to each entry without breaking O(1) operations?
company:airbnbdata-structurescachelinked-listhash-mapconcurrency

As asked

Airbnb uses tree structures internally, for example to represent hierarchical location data (continent, country, city, neighborhood). Implement serialization and deserialization of a binary tree. The serialized form can be any format you choose, as long as the deserialized tree is identical to the original. Discuss your format choice before coding.

Sample answer outline

A clean solution uses BFS with null markers, encoding as a comma-separated string. The candidate should explain why they chose BFS over DFS (often simpler to reconstruct), handle null nodes explicitly, and walk through reconstruction using a queue of values. Edge cases include single-node trees and all-null children.

Reference implementation (python)

Python
from collections import deque

def serialize(root):
    if not root:
        return 'null'
    result, queue = [], deque([root])
    while queue:
        node = queue.popleft()
        if node:
            result.append(str(node.val))
            queue.append(node.left)
            queue.append(node.right)
        else:
            result.append('null')
    return ','.join(result)

def deserialize(data):
    vals = data.split(',')
    if vals[0] == 'null':
        return None
    root = TreeNode(int(vals[0]))
    queue, i = deque([root]), 1
    while queue:
        node = queue.popleft()
        if vals[i] != 'null':
            node.left = TreeNode(int(vals[i]))
            queue.append(node.left)
        i += 1
        if vals[i] != 'null':
            node.right = TreeNode(int(vals[i]))
            queue.append(node.right)
        i += 1
    return root

Expect these follow-ups

  • How would you change your approach if the tree had up to 10 million nodes and you needed to minimize serialized size?
  • Can you serialize it in a way that lets you deserialize any subtree without reading the whole string?
company:airbnbalgorithmstreesserializationbfsstring

As asked

Write and explain a React Native FlatList setup for 10,000 message rows. The list should avoid avoidable row renders and keep scroll performance stable.

Sample answer outline

The candidate should stabilise renderItem and keyExtractor, memoise row components, and provide getItemLayout when row height is fixed. Initial render and window sizes should be tuned to the screen rather than set to huge values that hide poor rendering. The answer should mention image sizing, avoiding inline closures in rows, and measuring with the performance monitor. getItemLayout is only safe when row heights are known; variable height feeds need measurement or a different strategy. A common mistake is putting all messages into parent component state and changing the array identity on every tick.

Reference implementation (tsx)

TSX
import React, { memo, useCallback } from "react";
import { FlatList, Text, View } from "react-native";

type Message = { id: string; author: string; body: string };

const ROW_HEIGHT = 72;

const MessageRow = memo(function MessageRow({ message }: { message: Message }) {
  return (
    <View style={{ height: ROW_HEIGHT, padding: 12 }}>
      <Text>{message.author}</Text>
      <Text numberOfLines={2}>{message.body}</Text>
    </View>
  );
});

export function MessageList({ messages }: { messages: Message[] }) {
  const renderItem = useCallback(
    ({ item }: { item: Message }) => <MessageRow message={item} />,
    [],
  );

  return (
    <FlatList
      data={messages}
      keyExtractor={(item) => item.id}
      renderItem={renderItem}
      getItemLayout={(_, index) => ({
        length: ROW_HEIGHT,
        offset: ROW_HEIGHT * index,
        index,
      })}
      initialNumToRender={12}
      windowSize={7}
      removeClippedSubviews
    />
  );
}

Expect these follow-ups

  • What if row heights are variable because images load later?
  • How do you keep an inverted chat list anchored when new messages arrive?
  • When is FlashList a better choice?
flatlistreactperformance

As asked

Implement a small optimistic UI helper for a feature toggle. It should update immediately, call the server, and roll back if the request fails.

Sample answer outline

The core is to separate the previous state, optimistic state, server confirmation, and rollback path. A good candidate handles concurrent clicks by either disabling while pending or tracking request order so stale responses do not overwrite newer intent. The code should surface an error path the UI can show, not silently swallow failures. Interviewers want pragmatic product engineering: instant feedback without corrupting state. Common mistakes include not restoring the previous value, ignoring rejected promises, or allowing double toggles to race.

Reference implementation (typescript)

TypeScript
type SaveToggle = (enabled: boolean) => Promise<void>;

export async function applyOptimisticToggle(
  current: boolean,
  setEnabled: (next: boolean) => void,
  saveToggle: SaveToggle,
): Promise<void> {
  const next = !current;
  setEnabled(next);

  try {
    await saveToggle(next);
  } catch (error) {
    setEnabled(current);
    throw error;
  }
}

Expect these follow-ups

  • How would you handle two rapid clicks before the first request returns?
  • What changes if this value is also updated by another browser tab?
  • Where would you put analytics for successful and failed toggles?
typescriptoptimistic-uistate

As asked

Write a React hook called usePrevious<T> that returns the previous render's value of a given variable. Explain why a ref is the right primitive for this and why useState would not work.

Sample answer outline

Uses a ref initialized to undefined. After render, an effect stores the current value into the ref so the next render can read the stale value. useState would cause an extra render cycle. Strong answers note that the effect runs after paint so the previous value is always one render behind.

Reference implementation (typescript)

TypeScript
import { useRef, useEffect } from 'react';

function usePrevious<T>(value: T): T | undefined {
  // implement here
}

export default usePrevious;

Expect these follow-ups

  • How would you modify this to return both the previous and the value before that?
  • Why must the ref assignment happen in useEffect and not directly in the render function body?
reacthooksrefstypescript

As asked

Write a TypeScript function flattenDeep that takes an array of arbitrarily nested arrays (for example [[1, [2, 3]], [4, [5, [6]]]]) and returns a flat array of numbers, without using Array.prototype.flat. This kind of utility comes up when normalising tree-shaped API responses or recursive component data structures in frontend apps.

Sample answer outline

Recursive approach checks if each element is an array and recurses, otherwise pushes the value. Iterative approach uses a stack. Strong answers discuss time complexity O(n) where n is total elements, and note that deep recursion on extremely nested input risks a stack overflow, which the iterative version avoids.

Reference implementation (typescript)

TypeScript
type Nested<T> = (T | Nested<T>)[];

function flattenDeep<T>(arr: Nested<T>): T[] {
  // implement here
}

// flattenDeep([[1, [2, 3]], [4, [5, [6]]]])
// => [1, 2, 3, 4, 5, 6]

Expect these follow-ups

  • How would you add a depth parameter so it only flattens up to a certain level?
  • What is the time and space complexity of your recursive versus iterative solutions?
algorithmsarraysrecursiontypescript

As asked

Write a function promiseAll that takes an array of Promises and returns a single Promise that resolves with an array of results in the same order, or rejects immediately when any input Promise rejects. Do not use Promise.all internally.

Sample answer outline

Creates a result array and a counter. Each promise's .then handler stores the resolved value at its index and increments the counter, resolving the outer promise when all are done. The .catch on each promise rejects the outer promise. Strong answers handle empty input, and note that order is preserved by index even if promises resolve out of order.

Reference implementation (typescript)

TypeScript
function promiseAll<T>(promises: Promise<T>[]): Promise<T[]> {
  // implement here
}

// promiseAll([Promise.resolve(1), Promise.resolve(2)])
// => Promise<[1, 2]>

Expect these follow-ups

  • How would you implement Promise.allSettled so it never rejects but returns an array of status objects?
  • How would you add a concurrency limit so at most N promises run simultaneously?
javascriptpromisesasyncalgorithms

As asked

Design and implement an LRU (least recently used) cache class in TypeScript with get(key) and put(key, value) methods, both in O(1) time. The cache should accept a capacity at construction and evict the least recently used entry when full. This is a realistic building block for client-side query caches and memoization layers in a frontend app.

Sample answer outline

Uses a Map (which preserves insertion order) and exploits the fact that delete plus re-set moves the entry to the end. get deletes and re-inserts to mark as recently used. put evicts map.keys().next().value (the oldest entry) when at capacity. Should discuss why a doubly linked list plus a HashMap is the classical solution, but Map in JS gives the same complexity more simply.

Reference implementation (typescript)

TypeScript
class LRUCache {
  constructor(private capacity: number) {}

  get(key: number): number {
    // implement here
    return -1;
  }

  put(key: number, value: number): void {
    // implement here
  }
}

Expect these follow-ups

  • What is the time complexity of iterating over a JS Map and how does it differ from a plain object?
  • How would you make this LRU cache thread-safe if you were writing it in a shared worker context?
data-structuresalgorithmstypescriptcaching

As asked

Implement a deepEqual function in TypeScript that returns true if two values are deeply equal. It should handle nested objects, arrays, null, and primitives. Explain your handling of edge cases like circular references.

Sample answer outline

Checks reference equality first, then type/null, then Array.isArray for arrays, then compares key counts before recursing. Circular reference handling requires a WeakSet of visited object pairs or a path stack. Strong answers note that NaN !== NaN in JavaScript and may handle it explicitly.

Reference implementation (typescript)

TypeScript
function deepEqual(a: unknown, b: unknown): boolean {
  // implement here
  return false;
}

deepEqual({ x: [1, { y: 2 }] }, { x: [1, { y: 2 }] }); // true
deepEqual({ x: 1 }, { x: 2 });                          // false

Expect these follow-ups

  • How would you extend this to handle Map, Set, and Date objects correctly?
  • Why use a WeakSet rather than a Set for tracking visited objects?
typescriptalgorithmsrecursionobjects

As asked

Given a sorted array that has been rotated at an unknown pivot (for example [4,5,6,7,0,1,2]), write a TypeScript function that searches for a target value and returns its index, or -1 if not found, in O(log n). This pattern appears in frontend scenarios such as searching a time-ordered event log that wraps around midnight.

Sample answer outline

Modified binary search. At each step, one of the two halves is guaranteed to be sorted. Compare mid with lo to determine which half is sorted, then check if target falls in the sorted range to decide which side to recurse into. This maintains O(log n) without needing to find the pivot first.

Reference implementation (typescript)

TypeScript
function searchRotated(nums: number[], target: number): number {
  let lo = 0, hi = nums.length - 1;
  while (lo <= hi) {
    const mid = (lo + hi) >> 1;
    if (nums[mid] === target) return mid;
    // determine sorted half and narrow search
  }
  return -1;
}

Expect these follow-ups

  • How does the algorithm change if the array may contain duplicates?
  • Can you extend this to find the rotation pivot index in O(log n)?
algorithmsbinary-searcharraystypescript

As asked

Figma's constraint system lets a designer specify how a child element repositions or resizes when its parent frame is resized (e.g., pin to right edge, scale proportionally, center horizontally). Given a parent resize event, write the logic that computes the new position and size of a child node based on its horizontal and vertical constraint settings.

Sample answer outline

A strong answer enumerates the constraint options per axis (LEFT, RIGHT, LEFT_RIGHT, CENTER, SCALE) and writes a function for each that maps old parent dimensions plus old child geometry to new child geometry. LEFT pins the left edge offset constant. RIGHT pins the right edge offset constant. LEFT_RIGHT keeps both offsets constant (effectively stretching the child). CENTER keeps the child centered regardless of parent size. SCALE maintains the child's size as a proportion of the parent. The candidate should handle degenerate cases (parent shrinks to zero width, child is larger than the parent) gracefully.

Reference implementation (typescript)

TypeScript
type HConstraint = 'LEFT' | 'RIGHT' | 'LEFT_RIGHT' | 'CENTER' | 'SCALE';
type VConstraint = 'TOP' | 'BOTTOM' | 'TOP_BOTTOM' | 'CENTER' | 'SCALE';

interface Rect { x: number; y: number; width: number; height: number; }

function applyConstraints(
  child: Rect,
  oldParent: Rect,
  newParent: Rect,
  hConstraint: HConstraint,
  vConstraint: VConstraint
): Rect {
  // your implementation
}

Expect these follow-ups

  • How does your solution interact with Figma's Auto Layout feature, which has its own stacking and spacing rules?
  • What happens when two constraints conflict, such as LEFT_RIGHT on a child that also has a fixed minimum width?
company:figmalayoutconstraintscodinggeometryfigma-internals

As asked

Figma's Auto Layout feature makes frames behave like CSS flexbox: children are stacked horizontally or vertically with configurable gaps, padding, and alignment. If you were implementing this layout algorithm from scratch without using browser CSS, how would you compute the final position and size of each child?

Sample answer outline

A strong answer explains that Figma Auto Layout is inspired by CSS flexbox but differs in important ways: children have explicit sizing modes ('fixed', 'hug contents', 'fill container') rather than flex-shrink and flex-grow, and the layout algorithm must be understood on its own terms rather than as a direct mapping to CSS. The two-pass algorithm works as follows: first pass measures each child's intrinsic size, resolving 'hug contents' children by recursively laying out their own subtrees; then a distribution pass allocates remaining space proportionally among 'fill container' children. The candidate should handle all alignment options (start, center, end, and space-between), nested Auto Layout frames via recursion, and the edge case where 'fill container' children exceed the available space in the parent. A great answer notes that Figma runs this layout in C++ compiled to WASM to hit frame rate targets, and that wrapping support (analogous to flex-wrap) was added in 2023, which introduces a third sizing pass to determine row or column breaks before distributing space.

Expect these follow-ups

  • How does wrapping (like flex-wrap) complicate your two-pass algorithm?
  • If a child has both 'fill container' width and a 'hug contents' height, in which order do you compute its dimensions?
company:figmalayout-algorithmflexboxcodingauto-layoutfigma-internals

As asked

Two users are typing into the same plain-text string simultaneously. User A inserts 'X' at index 3 and at the same time User B deletes the character at index 5. These operations need to be transformed against each other so both clients end up with the same final string. Implement the transform function for insert and delete operations.

Sample answer outline

A strong answer defines operations as typed structs (insert: {type, index, char} and delete: {type, index}), and implements the four transform cases: transform(insert, insert), transform(insert, delete), transform(delete, insert), and transform(delete, delete). Each case adjusts the index of the incoming operation based on whether the concurrent operation was before or after it. The critical insight is that if two inserts land at the same index, a tiebreak rule (e.g., by client ID) must be used to make the transform deterministic. A great candidate writes tests for each case and discusses why this approach breaks down with rich text or complex nested structures.

Expect these follow-ups

  • What happens if operations are lost or arrive out of order? How do your transforms handle that?
  • How would you extend this to support a list of operations batched into a single transaction?
company:figmaoperational-transformationcollaborationcodingotmultiplayer

As asked

Write a function that converts a Notion block tree to a Markdown string. You only need to handle these block types: paragraph, heading_1, heading_2, heading_3, bulleted_list_item, numbered_list_item, and code. Children of list items should be indented. The rich_text field of each block is an array of objects with a plain_text property.

Sample answer outline

A strong solution uses a recursive function that dispatches on block type, uses a depth or indent-level parameter for nested lists, and correctly handles numbered list ordering by tracking a counter at each nesting level. Edge cases include blocks with empty rich_text, code blocks needing a language attribute (which Notion stores under block.code.language), and the difference between a paragraph inside a list item versus a standalone paragraph. Candidates who write clean, readable TypeScript or Python and explain their assumptions score well even if they miss an edge case.

Reference implementation (typescript)

TypeScript
type RichText = { plain_text: string };
type Block = {
  type: string;
  rich_text?: RichText[];
  children?: Block[];
  // code blocks have a nested object
  code?: { rich_text: RichText[]; language: string };
};

function toMarkdown(block: Block, depth = 0): string {
  // your implementation
}

Expect these follow-ups

  • How would you handle a toggle block, where the heading is visible but the children are collapsed?
  • What changes if you need to produce HTML instead of Markdown?
company:notiontree-traversalrecursionstring-processingnotion-apimarkdown

As asked

Notion auto-saves changes to blocks as the user types without making an API call on every keystroke. Implement a hook or utility that debounces saves to at most once every 500ms, ensures in-flight requests are not lost if the user navigates away, and tracks which blocks are 'dirty' (locally modified but not yet saved to the server).

Sample answer outline

A strong solution implements a debounce with clearTimeout/setTimeout, keeps a Map of blockId to pending changes (dirty state), flushes the map on save and clears it on success, and uses beforeunload or the Page Visibility API to trigger a synchronous or best-effort flush when the user is about to leave. A strong candidate also discusses what happens if the save fails (retry with exponential backoff, surface a 'connection lost' indicator) and how to handle concurrent saves if two blocks become dirty within the same debounce window. TypeScript generics make the hook reusable.

Reference implementation (typescript)

TypeScript
function useDebouncedSave(
  save: (blockId: string, data: unknown) => Promise<void>,
  delay = 500
) {
  // return markDirty(blockId, data) and flush()
}

Expect these follow-ups

  • If the network drops mid-save, how do you ensure the user's changes are not silently lost?
  • How would you show the user a 'Saving...' or 'Saved' indicator that accurately reflects the state?
company:notiondebounceautosavetypescripthooksdirty-state

As asked

Notion uses WebSockets for real-time collaboration. Implement a WebSocket client wrapper in TypeScript that: automatically reconnects with exponential backoff when the connection drops, gives up after 5 retries, emits events (connected, disconnected, message, error), and cleans up all listeners when the client is destroyed.

Sample answer outline

A strong solution uses an EventEmitter or a similar pattern, implements backoff as delay = baseDelay * 2^attempt (capped at a max), stores the retry count, clears any pending retry timeout on explicit close, and removes all WebSocket event listeners before reassigning this.ws to prevent memory leaks. The candidate should handle the case where the WebSocket opens and then immediately closes (jitter is good to mention), and discuss how the caller knows which messages were lost during a reconnect gap and whether it needs to replay them.

Reference implementation (typescript)

TypeScript
class RealtimeClient {
  private ws: WebSocket | null = null;
  private retries = 0;
  private readonly maxRetries = 5;
  private readonly baseDelay = 500; // ms

  connect(url: string): void { /* ... */ }
  close(): void { /* ... */ }
  on(event: string, cb: Function): void { /* ... */ }
}

Expect these follow-ups

  • If the server is down for 10 minutes, your client gives up after 5 retries. How does the UI communicate this to the user, and how does the user resume collaboration when the server comes back?
  • How do you distinguish a network drop from the server intentionally closing the connection (e.g., auth expired)?
company:notionwebsocketsreconnectexponential-backofftypescriptevent-emitter

As asked

Write a generic EventEmitter<Events> class in TypeScript where Events is a record mapping event names to their payload types. It should have on, off, and emit methods, all fully type-safe so TypeScript catches wrong payload types at the call site.

Sample answer outline

Uses a generic type parameter constrained to Record<string, unknown>. Handlers map is Map<keyof Events, Set<Function>>. on<K extends keyof Events>(event: K, handler: (payload: Events[K]) => void) is the key signature. emit<K extends keyof Events>(event: K, payload: Events[K]) passes through. Should also discuss whether to return a cleanup function from on.

Reference implementation (typescript)

TypeScript
type Events = {
  login: { userId: string };
  logout: void;
};

class EventEmitter<E extends Record<string, unknown>> {
  // implement on, off, emit
}

const emitter = new EventEmitter<Events>();
emitter.on('login', ({ userId }) => console.log(userId));

Expect these follow-ups

  • How would you add a once method that automatically unregisters after the first emission?
  • What are the memory leak risks with an EventEmitter and how would you expose a listenerCount method to help diagnose them?
typescriptdesign-patternsgenericsevents

As asked

Write a RateLimiter class in TypeScript that implements the token bucket algorithm. It should allow up to N requests per second, queue excess requests, and execute them as tokens become available. This is for throttling API calls from the browser.

Sample answer outline

Maintains a token count and a refill timestamp. Each request call checks for available tokens, refills based on elapsed time, and either executes immediately or queues with a scheduled retry. Should handle promise-based request functions and propagate resolution/rejection correctly. Discusses that setInterval for refill is simpler but less accurate than lazy refill on each call.

Reference implementation (typescript)

TypeScript
class RateLimiter {
  private tokens: number;
  private lastRefill: number;

  constructor(
    private maxTokens: number,
    private refillRate: number
  ) {
    this.tokens = maxTokens;
    this.lastRefill = Date.now();
  }

  async execute<T>(fn: () => Promise<T>): Promise<T> {
    // implement here
    return fn();
  }
}

Expect these follow-ups

  • How would you add per-endpoint limits so different API routes have different rate limits?
  • What is the difference between token bucket and leaky bucket rate limiting?
algorithmstypescriptasyncrate-limiting