Frontend Interview Questions - Medium
Medium-level frontend interview questions covering advanced React, Vue, state management, and performance optimization.
Q1: Explain React Context API and when to use it.
Answer:
Prop Drilling Problem
Context Solution
1import { createContext, useContext, useState } from 'react';
2
3// Create context
4const UserContext = createContext();
5
6// Provider component
7function App() {
8 const [user, setUser] = useState({ name: 'Alice', role: 'admin' });
9
10 return (
11 <UserContext.Provider value={{ user, setUser }}>
12 <Header />
13 <Main />
14 </UserContext.Provider>
15 );
16}
17
18// Consumer component (deep in tree)
19function UserProfile() {
20 const { user, setUser } = useContext(UserContext);
21
22 return (
23 <div>
24 <h1>{user.name}</h1>
25 <p>Role: {user.role}</p>
26 <button onClick={() => setUser({ ...user, name: 'Bob' })}>
27 Change Name
28 </button>
29 </div>
30 );
31}
Context Flow
When to Use:
- Theme (dark/light mode)
- User authentication
- Language/localization
- Global settings
When NOT to Use:
- Frequently changing data (causes re-renders)
- Complex state logic (use Redux/Zustand)
Q2: What is React useCallback and useMemo?
Answer:
useMemo
1import { useState, useMemo } from 'react';
2
3function ExpensiveComponent({ items }) {
4 const [count, setCount] = useState(0);
5
6 // Without useMemo: Recalculates on every render
7 // const total = items.reduce((sum, item) => sum + item.price, 0);
8
9 // With useMemo: Only recalculates when items change
10 const total = useMemo(() => {
11 console.log('Calculating total...');
12 return items.reduce((sum, item) => sum + item.price, 0);
13 }, [items]); // Dependency array
14
15 return (
16 <div>
17 <p>Total: ${total}</p>
18 <button onClick={() => setCount(count + 1)}>
19 Count: {count}
20 </button>
21 </div>
22 );
23}
useCallback
1import { useState, useCallback } from 'react';
2import { memo } from 'react';
3
4// Child component (memoized)
5const Button = memo(({ onClick, children }) => {
6 console.log('Button rendered');
7 return <button onClick={onClick}>{children}</button>;
8});
9
10function Parent() {
11 const [count, setCount] = useState(0);
12 const [other, setOther] = useState(0);
13
14 // Without useCallback: New function on every render
15 // const increment = () => setCount(count + 1);
16
17 // With useCallback: Same function reference
18 const increment = useCallback(() => {
19 setCount(c => c + 1);
20 }, []); // Empty deps: function never changes
21
22 return (
23 <div>
24 <p>Count: {count}</p>
25 <Button onClick={increment}>Increment</Button>
26 <button onClick={() => setOther(other + 1)}>
27 Other: {other}
28 </button>
29 </div>
30 );
31}
When to Use
Q3: Explain Vue Vuex state management.
Answer:
Vuex Architecture
Vuex Store
1import { createStore } from 'vuex';
2
3const store = createStore({
4 // State
5 state() {
6 return {
7 count: 0,
8 user: null,
9 items: []
10 }
11 },
12
13 // Getters (like computed properties)
14 getters: {
15 doubleCount(state) {
16 return state.count * 2;
17 },
18 itemCount(state) {
19 return state.items.length;
20 }
21 },
22
23 // Mutations (synchronous)
24 mutations: {
25 increment(state) {
26 state.count++;
27 },
28 setUser(state, user) {
29 state.user = user;
30 },
31 addItem(state, item) {
32 state.items.push(item);
33 }
34 },
35
36 // Actions (asynchronous)
37 actions: {
38 async fetchUser({ commit }, userId) {
39 const response = await fetch(`/api/users/${userId}`);
40 const user = await response.json();
41 commit('setUser', user);
42 },
43
44 incrementAsync({ commit }) {
45 setTimeout(() => {
46 commit('increment');
47 }, 1000);
48 }
49 }
50});
Using in Components
1<template>
2 <div>
3 <p>Count: {{ count }}</p>
4 <p>Double: {{ doubleCount }}</p>
5 <button @click="increment">Increment</button>
6 <button @click="incrementAsync">Increment Async</button>
7 </div>
8</template>
9
10<script>
11import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';
12
13export default {
14 computed: {
15 ...mapState(['count']),
16 ...mapGetters(['doubleCount'])
17 },
18 methods: {
19 ...mapMutations(['increment']),
20 ...mapActions(['incrementAsync'])
21 }
22}
23</script>
Q4: What is React useReducer and when to use it?
Answer:
Reducer Pattern
useReducer Example
1import { useReducer } from 'react';
2
3// Reducer function
4function reducer(state, action) {
5 switch (action.type) {
6 case 'increment':
7 return { count: state.count + 1 };
8 case 'decrement':
9 return { count: state.count - 1 };
10 case 'reset':
11 return { count: 0 };
12 case 'set':
13 return { count: action.payload };
14 default:
15 throw new Error(`Unknown action: ${action.type}`);
16 }
17}
18
19function Counter() {
20 const [state, dispatch] = useReducer(reducer, { count: 0 });
21
22 return (
23 <div>
24 <p>Count: {state.count}</p>
25 <button onClick={() => dispatch({ type: 'increment' })}>+</button>
26 <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
27 <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
28 <button onClick={() => dispatch({ type: 'set', payload: 10 })}>
29 Set to 10
30 </button>
31 </div>
32 );
33}
Complex State Example
1function todoReducer(state, action) {
2 switch (action.type) {
3 case 'add':
4 return {
5 ...state,
6 todos: [...state.todos, { id: Date.now(), text: action.text, done: false }]
7 };
8 case 'toggle':
9 return {
10 ...state,
11 todos: state.todos.map(todo =>
12 todo.id === action.id ? { ...todo, done: !todo.done } : todo
13 )
14 };
15 case 'delete':
16 return {
17 ...state,
18 todos: state.todos.filter(todo => todo.id !== action.id)
19 };
20 case 'setFilter':
21 return { ...state, filter: action.filter };
22 default:
23 return state;
24 }
25}
26
27function TodoApp() {
28 const [state, dispatch] = useReducer(todoReducer, {
29 todos: [],
30 filter: 'all'
31 });
32
33 // ... component implementation
34}
useState vs useReducer:
Q5: Explain Vue composables (reusable composition functions).
Answer:
Creating a Composable
1// useCounter.js
2import { ref, computed } from 'vue';
3
4export function useCounter(initialValue = 0) {
5 const count = ref(initialValue);
6 const doubled = computed(() => count.value * 2);
7
8 function increment() {
9 count.value++;
10 }
11
12 function decrement() {
13 count.value--;
14 }
15
16 function reset() {
17 count.value = initialValue;
18 }
19
20 return {
21 count,
22 doubled,
23 increment,
24 decrement,
25 reset
26 };
27}
Using Composables
1<script setup>
2import { useCounter } from './useCounter';
3
4const { count, doubled, increment, decrement, reset } = useCounter(10);
5</script>
6
7<template>
8 <div>
9 <p>Count: {{ count }}</p>
10 <p>Doubled: {{ doubled }}</p>
11 <button @click="increment">+</button>
12 <button @click="decrement">-</button>
13 <button @click="reset">Reset</button>
14 </div>
15</template>
Advanced Composable
1// useFetch.js
2import { ref, watchEffect } from 'vue';
3
4export function useFetch(url) {
5 const data = ref(null);
6 const error = ref(null);
7 const loading = ref(false);
8
9 async function fetchData() {
10 loading.value = true;
11 error.value = null;
12
13 try {
14 const response = await fetch(url.value);
15 data.value = await response.json();
16 } catch (e) {
17 error.value = e;
18 } finally {
19 loading.value = false;
20 }
21 }
22
23 // Re-fetch when URL changes
24 watchEffect(() => {
25 fetchData();
26 });
27
28 return { data, error, loading, refetch: fetchData };
29}
1<script setup>
2import { ref } from 'vue';
3import { useFetch } from './useFetch';
4
5const userId = ref(1);
6const url = computed(() => `/api/users/${userId.value}`);
7const { data, error, loading, refetch } = useFetch(url);
8</script>
9
10<template>
11 <div>
12 <div v-if="loading">Loading...</div>
13 <div v-else-if="error">Error: {{ error.message }}</div>
14 <div v-else-if="data">
15 <h1>{{ data.name }}</h1>
16 <p>{{ data.email }}</p>
17 </div>
18 <button @click="refetch">Refresh</button>
19 </div>
20</template>
Q6: What is React reconciliation and the key prop?
Answer:
Without Keys
With Keys
Key Prop Usage
1// ❌ Bad: Using index as key
2function BadList({ items }) {
3 return (
4 <ul>
5 {items.map((item, index) => (
6 <li key={index}>{item.name}</li>
7 ))}
8 </ul>
9 );
10}
11
12// ✅ Good: Using unique ID as key
13function GoodList({ items }) {
14 return (
15 <ul>
16 {items.map(item => (
17 <li key={item.id}>{item.name}</li>
18 ))}
19 </ul>
20 );
21}
Problems with Index as Key:
1// Initial: [A, B, C]
2<li key={0}>A <input /></li>
3<li key={1}>B <input /></li>
4<li key={2}>C <input /></li>
5
6// After deleting A: [B, C]
7<li key={0}>B <input /></li> // Was key={1}, now key={0}
8<li key={1}>C <input /></li> // Was key={2}, now key={1}
9
10// Input states get mixed up!
Q7: Explain CSS-in-JS and styled-components.
Answer:
styled-components
1import styled from 'styled-components';
2
3// Create styled component
4const Button = styled.button`
5 background: ${props => props.primary ? 'blue' : 'gray'};
6 color: white;
7 padding: 10px 20px;
8 border: none;
9 border-radius: 4px;
10 cursor: pointer;
11
12 &:hover {
13 opacity: 0.8;
14 }
15
16 &:disabled {
17 opacity: 0.5;
18 cursor: not-allowed;
19 }
20`;
21
22const Container = styled.div`
23 max-width: 1200px;
24 margin: 0 auto;
25 padding: 20px;
26`;
27
28// Usage
29function App() {
30 return (
31 <Container>
32 <Button primary>Primary Button</Button>
33 <Button>Secondary Button</Button>
34 <Button disabled>Disabled Button</Button>
35 </Container>
36 );
37}
Dynamic Styling
1const Card = styled.div`
2 padding: 20px;
3 background: ${props => props.theme.background};
4 color: ${props => props.theme.text};
5 border: 1px solid ${props => props.theme.border};
6 border-radius: 8px;
7
8 ${props => props.elevated && `
9 box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
10 `}
11
12 ${props => props.size === 'large' && `
13 padding: 40px;
14 font-size: 1.2rem;
15 `}
16`;
17
18// Usage
19<Card elevated size="large">
20 Content
21</Card>
Theming
1import { ThemeProvider } from 'styled-components';
2
3const lightTheme = {
4 background: '#ffffff',
5 text: '#000000',
6 border: '#cccccc'
7};
8
9const darkTheme = {
10 background: '#1a1a1a',
11 text: '#ffffff',
12 border: '#444444'
13};
14
15function App() {
16 const [isDark, setIsDark] = useState(false);
17
18 return (
19 <ThemeProvider theme={isDark ? darkTheme : lightTheme}>
20 <Card>Themed content</Card>
21 <button onClick={() => setIsDark(!isDark)}>
22 Toggle Theme
23 </button>
24 </ThemeProvider>
25 );
26}
Q8: What are React Portals?
Answer:
Portal Usage
1import { createPortal } from 'react-dom';
2
3function Modal({ isOpen, onClose, children }) {
4 if (!isOpen) return null;
5
6 // Render into document.body instead of parent
7 return createPortal(
8 <div className="modal-overlay" onClick={onClose}>
9 <div className="modal-content" onClick={e => e.stopPropagation()}>
10 {children}
11 <button onClick={onClose}>Close</button>
12 </div>
13 </div>,
14 document.body // Target container
15 );
16}
17
18function App() {
19 const [isOpen, setIsOpen] = useState(false);
20
21 return (
22 <div style={{ overflow: 'hidden', position: 'relative' }}>
23 <h1>My App</h1>
24 <button onClick={() => setIsOpen(true)}>Open Modal</button>
25
26 <Modal isOpen={isOpen} onClose={() => setIsOpen(false)}>
27 <h2>Modal Content</h2>
28 <p>This renders outside the parent DOM hierarchy</p>
29 </Modal>
30 </div>
31 );
32}
DOM Structure
Why Use Portals:
- Escape
overflow: hiddenorz-indexconstraints - Render modals at document root
- Tooltips that extend beyond parent
- Dropdowns that need to overlay everything
Q9: Explain Vue watchers vs computed properties.
Answer:
Computed Properties
1<script setup>
2import { ref, computed } from 'vue';
3
4const firstName = ref('John');
5const lastName = ref('Doe');
6
7// Computed: Cached, only recalculates when dependencies change
8const fullName = computed(() => {
9 console.log('Computing full name');
10 return `${firstName.value} ${lastName.value}`;
11});
12
13// Computed with getter and setter
14const fullNameEditable = computed({
15 get() {
16 return `${firstName.value} ${lastName.value}`;
17 },
18 set(value) {
19 const parts = value.split(' ');
20 firstName.value = parts[0];
21 lastName.value = parts[1];
22 }
23});
24</script>
25
26<template>
27 <div>
28 <input v-model="firstName" />
29 <input v-model="lastName" />
30 <p>Full name: {{ fullName }}</p>
31 <input v-model="fullNameEditable" />
32 </div>
33</template>
Watchers
1<script setup>
2import { ref, watch, watchEffect } from 'vue';
3
4const searchQuery = ref('');
5const results = ref([]);
6const loading = ref(false);
7
8// Watch: React to changes, perform side effects
9watch(searchQuery, async (newQuery, oldQuery) => {
10 console.log(`Query changed from "${oldQuery}" to "${newQuery}"`);
11
12 if (newQuery.length < 3) {
13 results.value = [];
14 return;
15 }
16
17 loading.value = true;
18 try {
19 const response = await fetch(`/api/search?q=${newQuery}`);
20 results.value = await response.json();
21 } finally {
22 loading.value = false;
23 }
24}, {
25 immediate: false, // Don't run on mount
26 deep: false // Don't watch nested properties
27});
28
29// watchEffect: Automatically tracks dependencies
30watchEffect(() => {
31 console.log(`Search query is: ${searchQuery.value}`);
32 // Automatically re-runs when searchQuery changes
33});
34</script>
When to Use Each
Q10: What is code splitting and lazy loading in React?
Answer:
React.lazy and Suspense
1import { lazy, Suspense } from 'react';
2
3// Lazy load components
4const Dashboard = lazy(() => import('./Dashboard'));
5const Profile = lazy(() => import('./Profile'));
6const Settings = lazy(() => import('./Settings'));
7
8function App() {
9 const [page, setPage] = useState('dashboard');
10
11 return (
12 <div>
13 <nav>
14 <button onClick={() => setPage('dashboard')}>Dashboard</button>
15 <button onClick={() => setPage('profile')}>Profile</button>
16 <button onClick={() => setPage('settings')}>Settings</button>
17 </nav>
18
19 <Suspense fallback={<div>Loading...</div>}>
20 {page === 'dashboard' && <Dashboard />}
21 {page === 'profile' && <Profile />}
22 {page === 'settings' && <Settings />}
23 </Suspense>
24 </div>
25 );
26}
Route-based Code Splitting
1import { BrowserRouter, Routes, Route } from 'react-router-dom';
2import { lazy, Suspense } from 'react';
3
4const Home = lazy(() => import('./pages/Home'));
5const About = lazy(() => import('./pages/About'));
6const Contact = lazy(() => import('./pages/Contact'));
7
8function App() {
9 return (
10 <BrowserRouter>
11 <Suspense fallback={<div>Loading page...</div>}>
12 <Routes>
13 <Route path="/" element={<Home />} />
14 <Route path="/about" element={<About />} />
15 <Route path="/contact" element={<Contact />} />
16 </Routes>
17 </Suspense>
18 </BrowserRouter>
19 );
20}
Loading Flow
Bundle Analysis
Benefits:
- Faster initial page load
- Better performance on slow networks
- Load features only when needed
- Improved user experience
Summary
Medium frontend topics:
- Context API: Avoid prop drilling, global state
- useCallback/useMemo: Performance optimization, memoization
- Vuex: Centralized state management for Vue
- useReducer: Complex state logic, predictable updates
- Vue Composables: Reusable composition functions
- Reconciliation: React's diffing algorithm, key prop importance
- CSS-in-JS: Scoped styles, dynamic styling, theming
- React Portals: Render outside parent DOM hierarchy
- Watchers vs Computed: Side effects vs derived values
- Code Splitting: Lazy loading, route-based splitting
These concepts enable building scalable, performant frontend applications.
Related Snippets
- Frontend Interview Questions - Easy
Easy-level frontend interview questions covering HTML, CSS, JavaScript, React, … - Frontend Interview Questions - Hard
Hard-level frontend interview questions covering advanced patterns, performance …