Choose React when preparing for a React-targeted role. Choose Svelte when its component model suits the team and the surrounding libraries cover the project requirements.
Choose React for a React-targeted role and Svelte when a role, team, or project already commits to Svelte.
Svelte vs ReactJS: The Quick Answer
The React interview questions guide shows the component, JavaScript, and browser skills to prepare for a React-targeted interview.
React is for candidates targeting React roles or joining a React codebase. Skip it as the first choice when the target work explicitly uses Svelte and preparation time is limited.
Svelte is for candidates targeting Svelte roles or teams choosing a compact component model for a new project. Skip it when the target role or project requires another framework.
Framework choice does not replace JavaScript practice. Tasks involving closures, arrays, events, and browser storage still matter in either implementation, so pair framework work with JavaScript coding interview practice.
Key Differences at a Glance
| Decision point | React | Svelte |
|---|---|---|
| Architecture | Components execute at runtime, then React reconciles rendered JSX with the existing UI | A compiler turns declarative components into JavaScript that performs targeted DOM updates |
| Component syntax | JavaScript functions return JSX | Single-file components combine script, markup, and optional styles |
| Local state | useState returns state and a setter | $state creates reactive state |
| Derived values | Calculate during rendering or memoize when needed | $derived declares side-effect-free reactive values |
| Effects | useEffect synchronizes with external systems | $effect tracks reactive reads, runs in the browser, and may return teardown logic |
| Lists | map() produces elements with stable key values | {#each} renders lists and accepts a stable key |
| Conditional UI | JavaScript expressions choose JSX | Template blocks such as {#if} control markup |
| Testing | Runner and component testing libraries depend on project setup | Official guidance recommends Vitest for Vite projects, including SvelteKit, without requiring one overall testing stack |
| Application framework | React is separate from Next.js | Svelte is separate from SvelteKit |
JSX is still JavaScript syntax transformed for React. The JSX runtime exercise is useful when an interview moves below component syntax into how JSX expressions become function calls.
The Same Interview Task in React and Svelte
Build a filterable task list with identical requirements:
- Show all, active, or completed tasks.
- Toggle a task with a labelled checkbox.
- Display the remaining active count.
- Preserve task identity while filtering.
- Save tasks to
localStorage. - Recover from missing or malformed stored data.
React implementation with Hooks and JSX
import { useEffect, useState } from 'react';
const seed = [
{ id: 1, text: 'Review closures', done: false },
{ id: 2, text: 'Test keyboard access', done: true },
{ id: 3, text: 'Practice stable keys', done: false }
];
function valid(value) {
return Array.isArray(value) &&
value.every(task =>
['string', 'number'].includes(typeof task.id) &&
typeof task.text === 'string' &&
typeof task.done === 'boolean'
) &&
new Set(value.map(task => task.id)).size === value.length;
}
function readTasks() {
if (typeof window === 'undefined') return seed;
try {
const raw = localStorage.getItem('interview-tasks');
if (raw === null) return seed;
const parsed = JSON.parse(raw);
return valid(parsed) ? parsed : seed;
} catch {
return seed;
}
}
export default function TaskList() {
const [tasks, setTasks] = useState(readTasks);
const [filter, setFilter] = useState('all');
const visible = tasks.filter(task =>
filter === 'all' || (filter === 'done' ? task.done : !task.done)
);
const remaining = tasks.filter(task => !task.done).length;
useEffect(() => {
try {
localStorage.setItem('interview-tasks', JSON.stringify(tasks));
} catch {
// The list still works when storage is unavailable.
}
}, [tasks]);
function toggle(id) {
setTasks(current =>
current.map(task =>
task.id === id ? { ...task, done: !task.done } : task
)
);
}
return (
<section aria-labelledby="task-heading">
<h2 id="task-heading">Interview tasks</h2>
<p>{remaining} remaining</p>
{['all', 'active', 'done'].map(value => (
<button
type="button"
key={value}
aria-pressed={filter === value}
onClick={() => setFilter(value)}
>
{value}
</button>
))}
<ul>
{visible.map(task => (
<li key={String(task.id)}>
<label>
<input
type="checkbox"
checked={task.done}
onChange={() => toggle(task.id)}
/>
{task.text}
</label>
</li>
))}
</ul>
</section>
);
}
React keeps the task array as owned state and calculates both the visible list and remaining count during rendering. The storage Effect has no subscription or timer to clean up.
Svelte implementation with Svelte 5 runes
<script>
const seed = [
{ id: 1, text: 'Review closures', done: false },
{ id: 2, text: 'Test keyboard access', done: true },
{ id: 3, text: 'Practice stable keys', done: false }
];
function valid(value) {
return Array.isArray(value) &&
value.every(task =>
['string', 'number'].includes(typeof task.id) &&
typeof task.text === 'string' &&
typeof task.done === 'boolean'
) &&
new Set(value.map(task => task.id)).size === value.length;
}
function readTasks() {
try {
if (typeof window === 'undefined') return seed;
const storage = window.localStorage;
const raw = storage.getItem('interview-tasks');
if (raw === null) return seed;
const parsed = JSON.parse(raw);
return valid(parsed) ? parsed : seed;
} catch {
return seed;
}
}
let tasks = $state(readTasks());
let filter = $state('all');
let visible = $derived(tasks.filter(task =>
filter === 'all' || (filter === 'done' ? task.done : !task.done)
));
let remaining = $derived(tasks.filter(task => !task.done).length);
$effect(() => {
try {
localStorage.setItem('interview-tasks', JSON.stringify(tasks));
} catch {
// The list still works when storage is unavailable.
}
});
function toggle(id) {
const task = tasks.find(item => item.id === id);
if (task) task.done = !task.done;
}
</script>
<section aria-labelledby="task-heading">
<h2 id="task-heading">Interview tasks</h2>
<p>{remaining} remaining</p>
{#each ['all', 'active', 'done'] as value}
<button
type="button"
aria-pressed={filter === value}
onclick={() => filter = value}
>
{value}
</button>
{/each}
<ul>
{#each visible as task (String(task.id))}
<li>
<label>
<input
type="checkbox"
checked={task.done}
onclick={() => toggle(task.id)}
/>
{task.text}
</label>
</li>
{/each}
</ul>
</section>
The Svelte component owns the same state. $derived keeps filtered tasks and the count out of writable state, while the keyed each block preserves identity.
What an Interviewer Sees in Each Solution
Run equivalent acceptance tests against both components. Start each seed-dependent case with interview-tasks removed. In separate Vite React and Svelte projects, save the implementations as src/TaskList.jsx and src/TaskList.svelte, then save these files beside them. Create the fixtures with npm create vite@latest react-task -- --template react and npm create vite@latest svelte-task -- --template svelte; run npm install, install the test dependencies shown below, and execute npx vitest run --environment jsdom.
For React, install vitest, jsdom, @testing-library/react, and @testing-library/user-event, then add src/TaskList.test.jsx:
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { cleanup, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import TaskList from './TaskList.jsx';
const key = 'interview-tasks';
const duplicateIds = JSON.stringify([
{ id: 1, text: 'Numeric', done: false },
{ id: '1', text: 'String', done: true }
]);
beforeEach(() => localStorage.removeItem(key));
afterEach(cleanup);
describe('TaskList', () => {
it('filters and toggles the seed tasks', async () => {
const user = userEvent.setup();
render(<TaskList />);
expect(screen.getByText('2 remaining')).toBeTruthy();
await user.click(screen.getByRole('button', { name: 'done' }));
expect(screen.getByLabelText('Test keyboard access')).toBeTruthy();
expect(screen.queryByLabelText('Review closures')).toBeNull();
await user.click(screen.getByRole('button', { name: 'all' }));
await user.click(screen.getByLabelText('Review closures'));
expect(screen.getByText('1 remaining')).toBeTruthy();
});
it('preserves a retained row’s DOM identity while filtering', async () => {
const user = userEvent.setup();
render(<TaskList />);
const row = screen.getByText('Test keyboard access').closest('li');
await user.click(screen.getByRole('button', { name: 'done' }));
expect(screen.getByText('Test keyboard access').closest('li')).toBe(row);
});
it('restores saved state after remounting', async () => {
const user = userEvent.setup();
const first = render(<TaskList />);
await user.click(screen.getByLabelText('Review closures'));
first.unmount();
render(<TaskList />);
expect(screen.getByLabelText('Review closures').checked).toBe(true);
});
it.each(['{', JSON.stringify({ tasks: [] }), duplicateIds])(
'falls back for invalid stored data: %s',
raw => {
localStorage.setItem(key, raw);
render(<TaskList />);
expect(screen.getByText('2 remaining')).toBeTruthy();
}
);
it('keeps labelled controls keyboard-operable', async () => {
const user = userEvent.setup();
render(<TaskList />);
expect(screen.getByRole('checkbox', { name: 'Review closures' })).toBeTruthy();
const done = screen.getByRole('button', { name: 'done' });
done.focus();
await user.keyboard('{Enter}');
expect(done.getAttribute('aria-pressed')).toBe('true');
});
it('renders seed data when storage access throws', () => {
const descriptor = Object.getOwnPropertyDescriptor(window, 'localStorage');
Object.defineProperty(window, 'localStorage', {
configurable: true,
get() { throw new DOMException('Blocked', 'SecurityError'); }
});
try {
render(<TaskList />);
expect(screen.getByText('2 remaining')).toBeTruthy();
} finally {
Object.defineProperty(window, 'localStorage', descriptor);
}
});
});
For Svelte, install vitest, jsdom, @testing-library/svelte, and @testing-library/user-event, then add src/TaskList.test.js:
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { cleanup, render, screen } from '@testing-library/svelte';
import userEvent from '@testing-library/user-event';
import TaskList from './TaskList.svelte';
const key = 'interview-tasks';
const duplicateIds = JSON.stringify([
{ id: 1, text: 'Numeric', done: false },
{ id: '1', text: 'String', done: true }
]);
beforeEach(() => localStorage.removeItem(key));
afterEach(cleanup);
describe('TaskList', () => {
it('filters and toggles the seed tasks', async () => {
const user = userEvent.setup();
render(TaskList);
expect(screen.getByText('2 remaining')).toBeTruthy();
await user.click(screen.getByRole('button', { name: 'done' }));
expect(screen.getByLabelText('Test keyboard access')).toBeTruthy();
expect(screen.queryByLabelText('Review closures')).toBeNull();
await user.click(screen.getByRole('button', { name: 'all' }));
await user.click(screen.getByLabelText('Review closures'));
expect(screen.getByText('1 remaining')).toBeTruthy();
});
it('preserves a retained row’s DOM identity while filtering', async () => {
const user = userEvent.setup();
render(TaskList);
const row = screen.getByText('Test keyboard access').closest('li');
await user.click(screen.getByRole('button', { name: 'done' }));
expect(screen.getByText('Test keyboard access').closest('li')).toBe(row);
});
it('restores saved state after remounting', async () => {
const user = userEvent.setup();
const first = render(TaskList);
await user.click(screen.getByLabelText('Review closures'));
first.unmount();
render(TaskList);
expect(screen.getByLabelText('Review closures').checked).toBe(true);
});
it.each(['{', JSON.stringify({ tasks: [] }), duplicateIds])(
'falls back for invalid stored data: %s',
raw => {
localStorage.setItem(key, raw);
render(TaskList);
expect(screen.getByText('2 remaining')).toBeTruthy();
}
);
it('keeps labelled controls keyboard-operable', async () => {
const user = userEvent.setup();
render(TaskList);
expect(screen.getByRole('checkbox', { name: 'Review closures' })).toBeTruthy();
const done = screen.getByRole('button', { name: 'done' });
done.focus();
await user.keyboard('{Enter}');
expect(done.getAttribute('aria-pressed')).toBe('true');
});
it('renders seed data when storage access throws', () => {
const descriptor = Object.getOwnPropertyDescriptor(window, 'localStorage');
Object.defineProperty(window, 'localStorage', {
configurable: true,
get() { throw new DOMException('Blocked', 'SecurityError'); }
});
try {
render(TaskList);
expect(screen.getByText('2 remaining')).toBeTruthy();
} finally {
Object.defineProperty(window, 'localStorage', descriptor);
}
});
});
Score each criterion 0 or 1, with 4/5 required to pass and no failure allowed for identity, validation, or accessibility:
- State ownership: React uses one
useStatetask array and Svelte one$statetask array. Duplicatingvisibleorremainingas writable state scores 0. - Derivation and updates: React derives during rendering and replaces changed objects; Svelte uses
$derivedand mutates the matching$stateproxy. Stale counts or direct mutation of ordinary React state score 0. - Identity and validation: Both reject IDs that collide after string normalization and key rows by that normalized value. Index keys or accepting
1beside"1"score 0. - Behavior and persistence: Filtering, toggling, remounting, malformed data, and unavailable storage pass the shared tests. An uncaught storage error or synchronized duplicate state scores 0.
- Accessibility: Checkboxes have visible labels and filter buttons expose pressed state and work from the keyboard. Click-only non-semantic controls score 0.
Cleanup is a follow-up question rather than an implementation score here because the exercise starts no ongoing resource. Listeners, subscriptions, repeating timers, and pending work that could outlive an Effect invocation generally need matching cleanup; these storage writes do not.
A controlled checkbox also exposes an interview candidate’s understanding of state ownership. The controlled and uncontrolled input exercise isolates that decision.
Performance, Ecosystem, and Production Fit
Neither architecture makes one framework faster in every application. Data flow, list size, rendering frequency, dependencies, and measurement conditions still decide the result.
Choose React when the target codebase or a required library uses React. Choose Svelte when the target codebase uses Svelte or its component model and available libraries meet the project requirements.
Both frameworks can be used with TypeScript. Testing quality depends more on behavior-focused tests and project configuration than the component syntax alone.
React is not Next.js, and Svelte is not SvelteKit. When routing, server rendering, prerendering, or full application structure enters the requirements, compare the application frameworks separately. SvelteKit supplies those application features for Svelte.
Which Should You Learn for Frontend Interviews?
For a React-targeted role, practice Hooks, immutable updates, JSX lists, stable keys, and when an Effect is actually necessary. Do not confuse React with React Native, which has a different rendering target and is covered in React vs React Native.
Choose Svelte for a Svelte-targeted role or when the project decision is already made. Use Svelte 5 runes so the solution demonstrates current state, derivation, and effect syntax.
For framework-agnostic interviews, implement the same task twice and compare test results rather than line counts. The invariant is one owned task collection: React replaces the array and changed object so useState observes a new value, while Svelte mutates the matching object inside a deeply reactive $state proxy. React derives filtered rows and counts during rendering; Svelte expresses the same dependency with $derived. Both attach events to semantic controls, normalize IDs for keyed identity, validate stored data before restoring it, and catch storage failures. Those behaviors transfer even though their syntax and reactivity mechanisms differ.
For a longer plan that includes premium questions, framework variants, and worked solutions, UIReady Premium Annual and UIReady Premium Monthly fit candidates preparing across multiple interview rounds.