Freshworks Frontend Interview Questions
SaaS products. Solid JavaScript and maintainable components.
Freshworks builds business software such as Freshdesk and Freshservice: large, form-heavy and table-heavy web applications with long lifetimes. It has used Ember alongside React over the years, so interviewers tend to test JavaScript and frontend fundamentals that do not depend on one framework.
Candidates commonly describe coding rounds on JavaScript and problem solving, a frontend round on components and the browser, and a manager round.
- Questions
- 15, 5 with free answers
- Interview rounds
- 4
- Last reviewed
- October 2026
Freshworks frontend interview process
The usual shape of the loop. A guide to what to prepare, not a promise of what you will get.
- 1
Coding round
JavaScript and problem-solving questions, online or live.
- 2
Frontend round
JavaScript internals, the DOM, CSS and component design.
- 3
Design or project deep dive
How you would structure a feature, or a detailed walk through something you built.
- 4
Hiring manager and HR
Team fit, expectations and logistics.
What interviewers tend to look for
- JavaScript that is not tied to a framework: prototypes, modules, events.
- Component design for complex forms and data tables.
- Accessibility, which matters for software sold to enterprises.
- Testing and maintainability.
Your Freshworks mock interview
15 questions. Type an answer under each one and an AI reviewer scores it and tells you what you missed. Stuck on one? Ask for a hint. A free account gets 3 AI replies a day.
Your mock interview
0 of 15 answered
Answer a question and your score shows up here.
JavaScript
5 questions- Question 1 of 15JavaScript
Freshworks interviewer
How does prototypal inheritance work, and what does the
classsyntax add?Model answer
Every object has a hidden link to another object, its prototype. When you read a property that the object does not have, the engine looks on the prototype, then on that object's prototype, and so on until it finds the property or reaches
null.A function's
prototypeproperty becomes the prototype of the objects it creates withnew. That is how all arrays share one copy ofmap.classis mostly syntax over the same mechanism: methods go on the prototype,extendslinks the chain andsupercalls the parent. It does add a few real things:#privatefields, static blocks, and the rule that a class cannot be called withoutnew.Object.create(proto)creates an object with a chosen prototype directly, andObject.getPrototypeOf(obj)reads it. - Question 2 of 15JavaScript
Freshworks interviewer
Implement Promise.all.
Model answer
function promiseAll(iterable) { return new Promise((resolve, reject) => { const items = Array.from(iterable); const results = new Array(items.length); let pending = items.length; if (pending === 0) return resolve(results); items.forEach((item, index) => { Promise.resolve(item).then((value) => { results[index] = value; // by index, not by finish order pending -= 1; if (pending === 0) resolve(results); }, reject); }); }); }What the interviewer checks:
- Results keep the input order, whichever promise finishes first.
- Plain values are accepted, which is what
Promise.resolve(item)handles. - The first rejection rejects the whole thing.
- An empty input resolves straight away with an empty array.
Expect a follow-up asking for
Promise.allSettled,Promise.anyorPromise.race. - Question 3 of 15JavaScript
Freshworks interviewer
Flatten a nested array without using Array.prototype.flat.
Model answer
function flatten(array, depth = Infinity) { const result = []; for (const item of array) { if (Array.isArray(item) && depth > 0) { result.push(...flatten(item, depth - 1)); } else { result.push(item); } } return result; } flatten([1, [2, [3, [4]]]]); // [1, 2, 3, 4] flatten([1, [2, [3, [4]]]], 1); // [1, 2, [3, [4]]]Follow-ups: write it iteratively with a stack so that very deep nesting cannot overflow the call stack, and flatten an object into dot-separated keys (
{ a: { b: 1 } }to{ 'a.b': 1 }). - Question 4 of 15JavaScript
Freshworks interviewer
How do ES modules differ from CommonJS, and why does tree shaking depend on ES modules?
Model answer
ES module
importandexportstatements are static. They sit at the top level and are resolved before any code runs, so a bundler can see exactly which exports are used and drop the rest. That removal is tree shaking.CommonJS
requireis an ordinary function call. It can run inside anif, with a computed path, and it returns a plain object, so the bundler cannot be sure what is unused.Other differences: ES module imports are live bindings to the exported variable, ES modules load asynchronously and support top-level
await, and they always run in strict mode.Tree shaking also needs the library to be side-effect free, which packages declare with
"sideEffects": falseinpackage.json. Dynamicimport()is the tool for code splitting: it returns a promise and creates a separate chunk. - Question 5 of 15JavaScript
Freshworks interviewer
Implement pipe and compose.
Model answer
Both combine functions into one.
piperuns them left to right,composeright to left.const pipe = (...fns) => (input) => fns.reduce((value, fn) => fn(value), input); const compose = (...fns) => (input) => fns.reduceRight((value, fn) => fn(value), input); const slugify = pipe( (text) => text.trim(), (text) => text.toLowerCase(), (text) => text.split(' ').join('-') ); slugify(' Hello World '); // "hello-world"Follow-ups: let the first function take several arguments, and write an async version that awaits each step (
reduceover a promise chain).
TypeScript
1 question- Question 6 of 15TypeScript
Freshworks interviewer
When would you use an enum, a union of string literals, or
as const?Model answer
A union of string literals (
'admin' | 'editor') exists only at compile time. It adds nothing to the bundle and autocompletes well. It is the usual choice.An
enumproduces a real object at runtime. Numeric enums also get a reverse mapping and accept any number, which is a known weak spot. Because enums are not plain JavaScript, some toolchains restrict them.When you need the values at runtime as well, for example to render a dropdown, write them once with
as constand derive the type:const ROLES = ['admin', 'editor', 'viewer'] as const; type Role = (typeof ROLES)[number]; // 'admin' | 'editor' | 'viewer' const LABELS = { admin: 'Administrator', editor: 'Editor', viewer: 'Read only', } satisfies Record<Role, string>;as constmakes the value deeply read-only and keeps the literal types.satisfieschecks a value against a type without widening it, soLABELS.adminis still known to be that exact string.
React
4 questions- Question 7 of 15React
Freshworks interviewer
When would you use Context, and what is its performance problem?
Model answer
Context passes a value to any component below a provider without threading it through props. It suits values that many components read and that change rarely: theme, locale, the signed-in user.
The problem: when the provider's value changes, every component that reads that context re-renders, even if the part it uses is the same. Context has no selectors.
Ways to limit that:
- Memoise the value object, so the provider's own re-render does not create a new one each time.
- Split one large context into several, and put state and its setter functions in separate contexts.
- Keep fast-changing values, such as a text input or a mouse position, out of context.
- When many components need different slices of shared state, use a store with selectors (Zustand, Redux Toolkit, or
useSyncExternalStoredirectly).
Context is a way to deliver a value, not a state manager. The state still lives in a
useStateoruseReducersomewhere. - Question 8 of 15React
Freshworks interviewer
What is the difference between a controlled and an uncontrolled component?
Model answer
In a controlled input, React state holds the value. You pass
valueandonChange, and every keystroke goes through state. That makes validation, formatting and conditional UI easy.In an uncontrolled input, the DOM holds the value. You give it a
defaultValueand read the result on submit, through a ref orFormData.// Controlled <input value={email} onChange={(e) => setEmail(e.target.value)} /> // Uncontrolled <form onSubmit={(event) => { event.preventDefault(); const data = new FormData(event.currentTarget); save(data.get('email')); }} > <input name="email" defaultValue="" /> </form>Uncontrolled inputs re-render less, which is why form libraries such as React Hook Form use them, and React 19 form actions work the same way. A file input is always uncontrolled.
The warning to recognise: an input whose
valuegoes fromundefinedto a string has switched from uncontrolled to controlled. Start it with an empty string. - Question 9 of 15React
Freshworks interviewer
How do you test React components?
Model answer
The usual stack is React Testing Library with Vitest or Jest, and Playwright or Cypress for end-to-end tests.
The principle: test what a user can see and do, not how the component is built. Find elements by role and label, interact with them, and assert on what appears.
test('shows an error for an invalid email', async () => { const user = userEvent.setup(); render(<SignupForm />); await user.type(screen.getByLabelText('Email'), 'not-an-email'); await user.click(screen.getByRole('button', { name: 'Sign up' })); expect(await screen.findByRole('alert')).toHaveTextContent('valid email'); });- Querying by role also checks that the markup is accessible.
- Use
findByqueries for anything that appears after an async update. - Mock the network at the request level with a tool such as MSW, not by mocking your own modules.
- Do not assert on state, on hooks being called or on class names.
A reasonable split: unit tests for pure functions and hooks, component tests for forms and other interactive pieces, and a few end-to-end tests for the paths that earn money.
- Question 10 of 15React
Freshworks interviewer
Which component composition patterns do you use? How do you avoid prop drilling?
Model answer
Composition means building a component out of the pieces it is given instead of configuring it with a growing list of flags.
childrenand slot props:<Card header={<Title />}>...</Card>beats<Card title showIcon iconPosition>.- Compound components:
<Tabs>,<Tabs.List>and<Tabs.Panel>share state through a private context, and the caller arranges them freely. - Custom hooks for sharing logic. They have replaced most higher-order components and render props.
- Controlled and uncontrolled modes: accept
valueandonChange, and fall back to internal state when they are absent.
Composition also fixes most prop drilling. If
PagepassesuserthroughLayoutandHeaderonly so thatAvatarcan read it, havePagerender<Avatar user={user} />and pass that element down. The components in between no longer know aboutuser.Use Context for what is left.
CSS
2 questions- Question 11 of 15CSS
Freshworks interviewer
How does the browser decide which CSS rule wins?
Model answer
When several declarations target the same property on the same element, the cascade checks, in order:
- Origin and importance:
!importantdeclarations beat normal ones. - Cascade layers: for normal declarations, a later
@layerbeats an earlier one, and styles outside any layer beat all layers. - Specificity.
- Source order: the last one written wins.
Specificity is compared as three numbers: ids, then classes, attributes and pseudo-classes, then element names and pseudo-elements.
#nav .linkis (1,1,0) and beats.menu .item .linkat (0,3,0), because ids are compared first. An inlinestyleattribute beats any selector.Useful details:
:where()always has zero specificity, while:is()and:not()take the specificity of their most specific argument.In practice, keep specificity low and flat by styling with single classes, and use
@layerto order resets, components and utilities instead of fighting with!important. - Origin and importance:
- Question 12 of 15CSS
Freshworks interviewer
What are CSS custom properties, and how would you build a dark theme with them?
Model answer
A custom property is a value you name with two dashes and read with
var(). Unlike a Sass variable, it exists at runtime: it cascades, it is inherited, and it can be changed by a class, a media query or JavaScript without rebuilding anything.:root { --bg: #ffffff; --text: #0f172a; --accent: #4f46e5; } [data-theme='dark'] { --bg: #0b1020; --text: #e2e8f0; --accent: #818cf8; } body { background: var(--bg); color: var(--text); }Components only use the variables, so switching the theme means changing one attribute on the root element.
- Default to the system setting with
@media (prefers-color-scheme: dark)and let the user override it. - Save the choice, and apply it with a small inline script in the head before the page paints. Otherwise the wrong theme flashes on load.
- Set
color-scheme: light darkso that form controls and scrollbars match. var(--gap, 8px)takes a fallback, andelement.style.setProperty('--x', value)sets one from JavaScript.
- Default to the system setting with
Browser, performance and system design
1 question- Question 13 of 15Browser, performance and system design
Freshworks interviewer
How do you make a web page accessible?
Model answer
- Use the right HTML element. A
buttonis focusable, works with Enter and Space and is announced as a button. Adivwith a click handler does none of that. The same goes fornav,main, headings in order, and alabelfor every input. - Keyboard. Everything must be reachable and usable without a mouse, in a sensible order, with a visible focus ring. When a dialog opens, move focus into it, keep it there, and return it to the trigger on close.
- Text alternatives. Images need
alttext, or an emptyaltif they are decorative. Icon-only buttons need an accessible name. - Colour. Body text needs a contrast ratio of at least 4.5 to 1, and colour should never be the only signal.
- ARIA, when HTML is not enough.
aria-expandedon a toggle,aria-livefor updates that appear without a page load, roles for custom widgets. No ARIA is better than wrong ARIA. - Motion and zoom. Respect
prefers-reduced-motion, and make sure the page works at 200% zoom.
For testing: use the page with the keyboard only, run an automated checker such as axe or Lighthouse, and try a screen reader on the main flows. Automated tools catch only part of the problems.
- Use the right HTML element. A
Machine coding
2 questions- Question 14 of 15Machine coding
Freshworks interviewer
Build a data table with sorting, search and pagination.
Model answer
Keep the raw rows and the controls in state:
sort(keyanddirection),queryandpage. Everything shown is derived from those on each render.const visibleRows = useMemo(() => { const filtered = rows.filter((row) => row.name.toLowerCase().includes(query.toLowerCase()) ); const sorted = sort.key ? [...filtered].sort((a, b) => { const result = a[sort.key] > b[sort.key] ? 1 : a[sort.key] < b[sort.key] ? -1 : 0; return sort.direction === 'asc' ? result : -result; }) : filtered; const start = (page - 1) * pageSize; return { total: sorted.length, rows: sorted.slice(start, start + pageSize) }; }, [rows, query, sort, page, pageSize]);- Copy before sorting.
sortchanges the array in place. - Go back to page 1 when the search or sort changes.
- Clicking a header cycles ascending, descending, none, and shows an arrow.
- Drive columns from a config array (
key,label,sortable, optionalrender). - Use real
tablemarkup, witharia-sorton the sorted header. - Show an empty state when nothing matches.
Follow-ups: row selection with a select-all checkbox that has an indeterminate state, doing the sorting and paging on the server, a sticky header, and virtualising for very large data sets.
- Copy before sorting.
- Question 15 of 15Machine coding
Freshworks interviewer
Build a toast notification system.
Model answer
The pieces: a store holding the list of toasts, a function any component can call to add one, and a single container that renders them.
const ToastContext = createContext(null); function ToastProvider({ children }) { const [toasts, setToasts] = useState([]); const dismiss = useCallback( (id) => setToasts((list) => list.filter((toast) => toast.id !== id)), [] ); const show = useCallback( (message, { type = 'info', duration = 4000 } = {}) => { const id = crypto.randomUUID(); setToasts((list) => [...list, { id, message, type }]); setTimeout(() => dismiss(id), duration); return id; }, [dismiss] ); return ( <ToastContext.Provider value={show}> {children} <div className="toast-region" role="status" aria-live="polite"> {toasts.map((toast) => ( <Toast key={toast.id} {...toast} onClose={() => dismiss(toast.id)} /> ))} </div> </ToastContext.Provider> ); }- Each toast has its own id and its own timer, so they disappear independently.
- A close button on each, and the timer paused while the pointer is over it.
- A limit on how many are visible, with the rest queued.
aria-live="polite"so that screen readers announce new toasts, andassertivefor errors.- A fixed container in a corner, rendered once near the root.
Follow-ups: enter and exit animations (the toast has to stay mounted until the exit animation ends), positions, and an action button such as Undo.
Practise for Freshworks
Reading answers is the easy half. These problems run in the browser against real tests.
