Kenapa ini penting
Kalau kamu melamar posisi front-end atau full-stack dengan fokus UI, banyak perusahaan menambahkan (atau mengganti sebagian) ronde algoritma dengan ronde front-end khusus. Front End Interview Playbook dari GreatFrontEnd (yang juga direkomendasikan Tech Interview Handbook untuk front-end system design) mendaftar enam format: algorithmic coding, JavaScript coding (implementasi fungsi atau struktur data yang berkaitan dengan front-end, seperti debounce dan Promise.all), user interface coding (membangun komponen, widget, atau app), system design, quiz (pertanyaan konsep berjawaban singkat), dan behavioral. Kombinasi persisnya berbeda per perusahaan, jadi tanyakan ke recruiter.
Kabar baiknya: ini wilayahmu. Kamu sudah memakai debounce di input pencarian, Promise.all untuk fetch paralel, dan event emitter di Node.js. Bedanya, di interview kamu harus bisa menulisnya dari nol dan menjelaskan detailnya (this, urutan hasil, error handling, cleanup), sambil berbicara.
Konsep inti
Utility yang paling sering diminta
| Utility | Inti konsep | Detail yang sering ditanya |
|---|---|---|
debounce(fn, wait) |
jalankan setelah input "diam" wait ms |
clearTimeout, meneruskan this dan argumen, cancel |
throttle(fn, wait) |
paling banyak sekali per wait ms |
leading vs trailing call |
Promise.all |
tunggu semua, gagal kalau satu gagal | urutan hasil sesuai input, array kosong, nilai non-promise |
| Event emitter | on, off, once, emit |
unsubscribe saat emit, listener dipanggil berurutan |
| Deep clone | salin bersarang | Date, Map, Set, referensi melingkar, structuredClone bawaan |
Debounce vs throttle dengan analogi lift: debounce seperti pintu lift yang menunggu sampai tidak ada orang masuk selama 3 detik, baru menutup. Throttle seperti lift yang berangkat paling sering sekali per menit, siapa pun yang menekan tombol. Debounce cocok untuk input pencarian (kirim request setelah user berhenti mengetik); throttle cocok untuk event scroll atau resize (update posisi secara berkala).
React live coding
Soal umum: autocomplete/typeahead, todo list dengan filter, tabs, accordion, modal, star rating, infinite scroll, progress bar, data table dengan sort. Yang dinilai bukan hanya "jalan", tapi:
- State yang minimal: jangan simpan data turunan di state (misalnya
filteredItemsbisa dihitung dariitems+query). - Effect yang benar: dependency lengkap, cleanup untuk timer dan request, menangani race condition (respons lama datang setelah respons baru).
- Status lengkap: loading, error, empty, success.
- Aksesibilitas: label terhubung ke input, peran ARIA yang tepat, bisa dipakai dengan keyboard.
- Komunikasi: jelaskan "I'll start with the simplest working version, then add debouncing and error handling."
Pertanyaan konsep yang sering muncul
| Topik | Contoh pertanyaan | Inti jawaban |
|---|---|---|
| Event loop | "What's the output order of setTimeout vs Promise.then?" | microtask (Promise) dijalankan sebelum task berikutnya (setTimeout) |
| Rendering | "What happens when you type a URL and press Enter?" | DNS, TCP/TLS, HTTP, parsing HTML, CSSOM, render tree, layout, paint, composite |
| Event | "Explain event delegation." | pasang satu listener di induk, manfaatkan bubbling, cek event.target |
| Keamanan | "How do you prevent XSS?" | escape output, hindari dangerouslySetInnerHTML/innerHTML dengan input user, CSP |
| Aksesibilitas | "How would you make a custom dropdown accessible?" | elemen semantik dulu, ARIA role/state, fokus keyboard, label |
| Performa | "How would you improve LCP?" | optimasi gambar hero, preload, kurangi render-blocking resource, SSR/SSG |
| React | "Why do keys matter in lists?" | key stabil membantu reconciliation; index sebagai key bermasalah saat urutan berubah |
| Next.js | "When would you use a Server Component?" | data fetching di server, mengurangi JS di client; Client Component untuk interaktivitas |
Jawab konsep dengan pola: definisi singkat → contoh konkret dari pengalamanmu → trade-off. Contoh: "Event delegation means attaching one listener to a parent instead of one per child. In my last project, a table with a thousand rows used one click handler on the tbody. The trade-off is that you need to check event.target carefully, and some events like focus don't bubble the same way."
Contoh kode
Lima utility JavaScript
// 1. debounce: jalankan fn hanya setelah "diam" selama wait ms
function debounce(fn, wait) {
let timer = null;
function debounced(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), wait); // arrow function menangkap this dari debounced
}
debounced.cancel = () => clearTimeout(timer);
return debounced;
}
// 2. throttle: paling banyak sekali per wait ms (leading call, sisanya dibuang)
function throttle(fn, wait) {
let last = -Infinity;
return function (...args) {
const now = Date.now();
if (now - last >= wait) { last = now; fn.apply(this, args); }
};
}
// 3. Promise.all buatan sendiri
function promiseAll(items) {
return new Promise((resolve, reject) => {
const results = new Array(items.length);
let remaining = items.length;
if (remaining === 0) return resolve([]);
items.forEach((item, i) => {
Promise.resolve(item).then(value => { // nilai biasa juga boleh
results[i] = value; // jaga urutan sesuai input, bukan urutan selesai
if (--remaining === 0) resolve(results);
}, reject); // gagal satu = gagal semua
});
});
}
// 4. Event emitter
class EventEmitter {
#events = new Map();
on(name, cb) {
if (!this.#events.has(name)) this.#events.set(name, new Set());
this.#events.get(name).add(cb);
return () => this.off(name, cb); // kembalikan fungsi unsubscribe
}
off(name, cb) { this.#events.get(name)?.delete(cb); }
once(name, cb) {
const wrapper = (...args) => { this.off(name, wrapper); cb(...args); };
return this.on(name, wrapper);
}
emit(name, ...args) {
const listeners = this.#events.get(name);
if (!listeners) return false;
for (const cb of [...listeners]) cb(...args); // salin dulu: aman kalau ada off() saat emit
return true;
}
}
// 5. deep clone dengan dukungan Date, Map, Set, Array, dan referensi melingkar
function deepClone(value, seen = new WeakMap()) {
if (value === null || typeof value !== 'object') return value;
if (seen.has(value)) return seen.get(value);
if (value instanceof Date) return new Date(value);
if (value instanceof Map) { const m = new Map(); seen.set(value, m); value.forEach((v, k) => m.set(deepClone(k, seen), deepClone(v, seen))); return m; }
if (value instanceof Set) { const s = new Set(); seen.set(value, s); value.forEach(v => s.add(deepClone(v, seen))); return s; }
const copy = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));
seen.set(value, copy);
for (const key of Reflect.ownKeys(value)) copy[key] = deepClone(value[key], seen);
return copy;
}
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function main() {
// debounce: 5 panggilan cepat -> 1 eksekusi dengan argumen terakhir
const calls = [];
const search = debounce(q => calls.push(q), 50);
for (const q of ['r', 're', 'rea', 'reac', 'react']) search(q);
await sleep(80);
console.log('debounce:', calls);
// → debounce: [ 'react' ]
// throttle: dipanggil tiap 10ms selama ~100ms, wait 40ms
let fired = 0; const onScroll = throttle(() => fired++, 40);
for (let i = 0; i < 10; i++) { onScroll(); await sleep(10); }
console.log('throttle dipanggil 10x, jalan:', fired, 'kali (sekitar 3, tergantung timer)');
// → throttle dipanggil 10x, jalan: 3 kali (sekitar 3, tergantung timer)
// promiseAll: urutan hasil mengikuti input walau yang pertama paling lambat
console.log(await promiseAll([sleep(30).then(() => 'lambat'), 'langsung', Promise.resolve(42)]));
// → [ 'lambat', 'langsung', 42 ]
console.log(await promiseAll([]));
// → []
try { await promiseAll([sleep(10), Promise.reject(new Error('boom'))]); }
catch (e) { console.log('ditolak:', e.message); }
// → ditolak: boom
// EventEmitter: on, once, unsubscribe
const bus = new EventEmitter(); const log = [];
const unsub = bus.on('msg', m => log.push('A:' + m));
bus.once('msg', m => log.push('B:' + m));
bus.emit('msg', 1); bus.emit('msg', 2); unsub(); bus.emit('msg', 3);
console.log(log, bus.emit('nothing'));
// → [ 'A:1', 'B:1', 'A:2' ] false
// deepClone: objek bersarang, Set, Date, referensi melingkar
const original = { name: 'Ana', tags: new Set(['js']), born: new Date('2000-01-01'), nested: { a: [1, { b: 2 }] } };
original.self = original;
const c = deepClone(original);
c.nested.a[1].b = 99; c.tags.add('ts');
console.log(original.nested.a[1].b, c.nested.a[1].b, original.tags.size, c.tags.size, c.self === c, c.born instanceof Date);
// → 2 99 1 2 true true
const sc = structuredClone(original); // bawaan browser modern dan Node 17+
console.log('structuredClone:', sc.self === sc, sc.born instanceof Date);
// → structuredClone: true true
}
main();
Jumlah eksekusi throttle bergantung pada ketepatan timer sistem; di run ini hasilnya 3. Itu sebabnya test berbasis waktu nyata sebaiknya memakai fake timers (misalnya vi.useFakeTimers() di Vitest) di proyek sungguhan. Tentang deepClone: di interview, sebutkan bahwa di kode produksi kamu akan memakai structuredClone bawaan, lalu jelaskan batasannya (tidak bisa menyalin fungsi atau DOM node, dan prototype class tidak dipertahankan).
Live coding React: Autocomplete
Komponen ini memakai debounce sebagai hook, AbortController untuk membuang respons basi, status lengkap, dan atribut ARIA dasar untuk combobox.
import { useEffect, useId, useState } from 'react';
// Hook kecil: nilai yang baru berubah setelah user berhenti mengetik
function useDebouncedValue(value, delay) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const t = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(t); // cleanup: batalkan timer lama
}, [value, delay]);
return debounced;
}
export function Autocomplete({ fetchSuggestions, delay = 300 }) {
const [query, setQuery] = useState('');
const [items, setItems] = useState([]);
const [status, setStatus] = useState('idle'); // idle | loading | success | error
const debounced = useDebouncedValue(query.trim(), delay);
const listId = useId();
useEffect(() => {
if (!debounced) { setItems([]); setStatus('idle'); return; }
const controller = new AbortController();
setStatus('loading');
fetchSuggestions(debounced, controller.signal)
.then(result => { setItems(result); setStatus('success'); })
.catch(err => { if (err.name !== 'AbortError') setStatus('error'); });
return () => controller.abort(); // cegah race condition: respons lama dibuang
}, [debounced, fetchSuggestions]);
return (
<div>
<label htmlFor={listId + '-input'}>Cari framework</label>
<input
id={listId + '-input'}
value={query}
onChange={e => setQuery(e.target.value)}
role="combobox"
aria-expanded={items.length > 0}
aria-controls={listId}
aria-autocomplete="list"
/>
{status === 'loading' && <p role="status">Memuat…</p>}
{status === 'error' && <p role="alert">Gagal memuat. Coba lagi.</p>}
{status === 'success' && items.length === 0 && <p>Tidak ada hasil</p>}
<ul id={listId} role="listbox">
{items.map(item => <li key={item} role="option" aria-selected="false">{item}</li>)}
</ul>
</div>
);
}
Komponen di atas diuji dengan React 19.3.1 + jsdom 30.1.3: fetchSuggestions palsu dengan jeda 20 ms, debounce 30 ms, lalu mensimulasikan user mengetik. Hasil nyata run tersebut (format: label | isi list | pesan status):
# → setelah ketik "re" (belum lewat debounce) | (kosong) |
# → lewat debounce, sedang fetch | (kosong) | Memuat…
# → fetch selesai | React,Redux,Remix |
# → ketik "n" | Next.js,Nuxt |
# → ketik "zzz" | Tidak ada hasil
# → server error | (kosong) | Gagal memuat. Coba lagi.
# → aria-expanded: false | label terhubung: true
Yang bisa kamu tawarkan sebagai pengembangan kalau waktu masih ada: navigasi keyboard (panah atas/bawah, Enter, Escape) dengan aria-activedescendant, highlight bagian yang cocok, cache hasil per query, dan pesan "Memuat…" yang tidak menghapus hasil lama. Untuk combobox yang lengkap, rujuk pola combobox di ARIA Authoring Practices Guide (APG) dari W3C.
Kesalahan umum
- Debounce yang kehilangan
thisatau argumen. Cara benar: simpan...argsdan pakaifn.apply(this, args)di dalam arrow function. Promise.allyang mengisi hasil denganpush. Urutan jadi mengikuti siapa yang selesai duluan. Cara benar:results[i] = value.- Lupa cleanup di
useEffect. Timer dan request tetap berjalan setelah komponen unmount atau query berubah. Cara benar: kembalikan fungsi cleanup (clearTimeout,controller.abort()). - Menyimpan data turunan di state. Misalnya
filteredItemsdisimpan terpisah lalu lupa disinkronkan. Cara benar: hitung saat render, atauuseMemokalau mahal. <div onClick>sebagai tombol. Tidak bisa difokus keyboard dan tidak diumumkan screen reader. Cara benar:<button>; gunakan ARIA hanya kalau elemen semantik tidak ada.
Latihan
- Mudah, Tulis
once(fn): fungsi yang hanya menjalankanfnpada panggilan pertama dan mengembalikan hasil yang sama di panggilan berikutnya. - Sedang, Tulis
allSettled(items)sepertiPromise.allSettled: tidak pernah reject, hasilnya{ status, value }atau{ status, reason }. - Menantang, Tulis
promisePool(tasks, limit): jalankan array fungsi async dengan paling banyaklimitberjalan bersamaan, hasil sesuai urutan input.
Lihat solusi
const sleep = ms => new Promise(r => setTimeout(r, ms));
// 1. Simpan flag dan hasil di closure
function once(fn) { let done = false, result; return function (...args) { if (!done) { done = true; result = fn.apply(this, args); } return result; }; }
let n = 0; const init = once(() => ++n);
console.log(init(), init(), init(), n);
// → 1 1 1 1
// 2. Ubah setiap promise menjadi promise yang selalu resolve, lalu Promise.all
function allSettled(items) {
return Promise.all(items.map(p => Promise.resolve(p).then(
value => ({ status: 'fulfilled', value }),
reason => ({ status: 'rejected', reason: reason.message ?? reason }))));
}
// 3. Buat 'limit' worker yang mengambil tugas berikutnya dari indeks bersama
async function promisePool(tasks, limit) {
const results = new Array(tasks.length); let next = 0, running = 0, maxRunning = 0;
async function worker() {
while (next < tasks.length) {
const i = next++; running++; maxRunning = Math.max(maxRunning, running);
try { results[i] = await tasks[i](); } finally { running--; }
}
}
await Promise.all(Array.from({ length: Math.min(limit, tasks.length) }, worker));
return { results, maxRunning };
}
(async () => {
console.log(JSON.stringify(await allSettled([1, Promise.reject(new Error('x')), sleep(5).then(() => 'ok')])));
// → [{"status":"fulfilled","value":1},{"status":"rejected","reason":"x"},{"status":"fulfilled","value":"ok"}]
const tasks = [30, 10, 20, 10, 5].map((ms, i) => () => sleep(ms).then(() => i));
console.log(JSON.stringify(await promisePool(tasks, 2)));
// → {"results":[0,1,2,3,4],"maxRunning":2}
})();
promisePool aman dari race condition karena JavaScript single-threaded: next++ tidak pernah dijalankan dua worker secara bersamaan. Ini poin bagus untuk dijelaskan. Kasus nyata: membatasi upload paralel atau request ke API dengan rate limit.
Ringkasan
- Format front-end menurut GreatFrontEnd: algorithmic, JavaScript coding, UI coding, system design, quiz, behavioral; tanyakan kombinasi pastinya ke recruiter.
- Debounce menunggu diam, throttle membatasi frekuensi; teruskan
thisdan argumen. Promise.allmenjaga urutan input, resolve[]untuk array kosong, dan reject saat satu gagal.- Di React live coding: state minimal, effect dengan cleanup, penanganan race condition, status lengkap, dan aksesibilitas.
- Jawab konsep dengan pola definisi → contoh dari pengalaman → trade-off.