React Interview Experience
This blog is about my recent React interview experiences and some interesting questions that were asked. These questions might help you prepare for your next interview.
Let’s get started!
Guess The Output
I was given the following code snippet, and I was supposed to guess the output.
for(var i=0; i<3; i++) {
setTimeout(() => {
console.log(i)
}, 1000)
}
It should be 0, 1, 2
, right?
The above code snippet might look simple, but it can lead to some unexpected behavior if you are not familiar with how JavaScript closures work. Let’s take a closer look at what’s happening.
We expect the code to log the numbers 0, 1, 2
to the console with a delay of 1 second between each log. However, the output we get is 3, 3, 3
.
The reason for this unexpected behavior is that the setTimeout
function executes the callback function after a certain amount of time has passed. And as var
is function scoped and not block scoped, the value of i
will be taken from the global scope which will be 3
.
Solution
There is more than one solution here. Let’s see them one by one.
Use let
The let
keyword can solve the problem in the original code because let
is block-scoped.
When we replace the var
keyword with let
in the original code, a new i
variable is created for each iteration of the loop, and its value is captured by the setTimeout
callback function. This means that each callback function logs the value of its own i
variable, which is the expected behavior.
for(let i=0; i<3; i++) {
setTimeout(() => {
console.log(i)
}, 1000)
}
Use bind
This is the solution I had come up with because the interviewer wanted a solution without using let
.
Calling bind
method on a function returns a new function by setting the this
argument to the value we pass. So in this case, I passed i
as the this
parameter.
for(let i=0; i<3; i++) {
setTimeout((() => {
console.log(i)
}).bind({ i }), 1000)
}
Use Closures
To fix this issue, we need to create a closure around the callback function to capture the current value of i
at each iteration.
for(var i=0; i<3; i++) {
function closure (index) {
setTimeout(() => {
console.log(index)
}, 1000)
}
closure(i)
}
In this modified code, we create a new scope for i
using another function and pass the current value of i
to the function as a parameter. This creates a closure around the callback function, and the value of i
is captured at each iteration. Now, when the callback function is executed, it logs the correct value of i
.
useState vs useReducer
In React, both useState
and useReducer
are used to manage the state of a component.
So, what’s the difference between useState
and useReducer
?
useReducer
is often used when you have more complex state logic that involves multiple sub-values or when the next state depends on the previous state. It can also make your code more organized and easier to reason about, especially for larger and more complex applications.
In contrast, useState
is a more straightforward way to manage state, and is often sufficient for simpler use cases.
Let’s take an example of a shopping cart application that allows users to add items to their cart, remove items from their cart, and adjust the number of items in their cart.
Using useState
:
function ShoppingCart() {
const [items, setItems] = useState([]);
const addItem = (item) => {
setItems([...items, item]);
};
const removeItem = (index) => {
const newItems = [...items];
newItems.splice(index, 1);
setItems(newItems);
};
const adjustQuantity = (index, quantity) => {
const newItems = [...items];
newItems[index].quantity = quantity;
setItems(newItems);
};
return (
// JSX code
)
}
Using useReducer
:
const initialState = { items: [] };
function reducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
return { items: [...state.items, action.payload] };
case 'REMOVE_ITEM':
const newItems = [...state.items];
newItems.splice(action.payload, 1);
return { items: newItems };
case 'ADJUST_QUANTITY':
const adjustedItems = [...state.items];
adjustedItems[action.payload.index].quantity = action.payload.quantity;
return { items: adjustedItems };
default:
return state;
}
}
function ShoppingCart() {
const [state, dispatch] = useReducer(reducer, initialState);
const addItem = (item) => {
dispatch({ type: 'ADD_ITEM', payload: item });
};
const removeItem = (index) => {
dispatch({ type: 'REMOVE_ITEM', payload: index });
};
const adjustQuantity = (index, quantity) => {
dispatch({
type: 'ADJUST_QUANTITY',
payload: { index, quantity },
});
};
return (
// JSX code
)
}
Although the code using useState
is compact, writing code with useReducer
would be more declarative and provides greater readability for complex state items.
useCallback and useMemo
useCallback
useCallback
is a hook in React that is used to memoize functions. It is primarily used to optimize the performance of child components that receive function props, as it helps to avoid unnecessary re-rendering caused by a new function reference being created on each render.
Here’s an example:
// ProductPage.js
import { useCallback } from 'react';
import ShippingForm from './ShippingForm.js';
export default function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
return (
<div className={theme}>
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
function post(url, data) {
// Imagine this sends a request...
console.log('POST /' + url);
console.log(data);
}
// ShippingForm.js
import { memo, useState } from 'react';
const ShippingForm = memo(function ShippingForm({ onSubmit }) {
function handleSubmit() {
onSubmit(orderDetails);
}
return (
<form onSubmit={handleSubmit}>
{/* ... */}
</form>
);
});
export default ShippingForm;
The ShippingForm
component re-renders on every re-render of the parent component ProductPage
. We only want the ShippingForm
function to re-render when handleSubmit
function changes, and not on theme
changes.
The handleSubmit
function is defined using useCallback
, which memoizes the function so that it does not change on every re-render unless the dependencies (productId
and referrer
) change.
The ShippingForm
component is wrapped in memo
, which also helps to prevent unnecessary re-renders. i.e. only re-render when any prop changes.
This ensures that the ShippingForm
component does not unnecessarily re-render due to a change in the handleSubmit
function reference.
useMemo
useMemo
is a React Hook that lets you cache the result of a calculation between re-renders.
Let’s see the following example:
import { useMemo, useState } from 'react';
import expensiveCalculation from './expensiveCalculation'
function App() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const result = useMemo(() => {
return expensiveCalculation(a, b)
}, [a, b]);
return (
<div>
<p>A: {a}</p>
<button onClick={() => setA(a + 1)}>Increment A</button>
<p>B: {b}</p>
<button onClick={() => setB(b + 1)}>Increment B</button>
<p>Result: {result}</p>
</div>
);
}
In this example, the result
variable is calculated using the useMemo
hook. It is memoized to prevent unnecessary recalculations when the component is re-rendered.
The useMemo
hook takes a function as its first argument that calculates the result
value. The second argument is an array of dependencies that tells React when to recalculate the result
. In this case, the result
is recalculated whenever a
or b
changes.
Redux Vs Context API
Redux and Context API are both state management solutions in React. However, they have different use cases and can be used for different purposes.
Other than the syntactical differences, the major difference that matters is the performance.
Context API re-renders the whole part of the subtree which is wrapped inside the context provider. On the other hand, with Redux, you can selectively subscribe to specific parts of the store to avoid unnecessary updates, which can improve performance.
Also, Redux can be more efficient since it provides the ability to use memoized selectors, which can avoid unnecessary re-renders.
Manage The Focus Of Elements Using React
Problem statement: Suppose you have a component named Input
and an outer component rendering the Input
component twice. Write a code for focusing on the first Input
when the outer div
is clicked, and also focus on the respective inputs when they are clicked.
Solution: I created a component called Input
and exposed the ref
to the parent component using forwardRef
. Also, I used event.stopPropagation
for stopping the bubbling of the event to the outer parent.
import "./styles.css";
import { forwardRef, useRef } from "react";
export default function App() {
const ref1 = useRef(null);
const ref2 = useRef(null);
return (
<div
className="App"
style={{ border: "1px solid black", padding: "20px" }}
onClick={() => {
ref1.current.focus();
}}
>
<Input ref={ref1} />
<Input ref={ref2} />
</div>
);
}
const Input = forwardRef(function (props, ref) {
return (
<input
type="text"
ref={ref}
onClick={(e) => {
e.stopPropagation();
}}
/>
);
});
Why is useRef used?
useRef
is used for storing a value between re-renders of a component. It is similar to storing state with the major difference that useRef
does not trigger re-renders on changes.
Why not use a normal variable then?
The reason is a normal variable declared using const/let/var
will have their current value overridden by the default value because the component function gets executed again, and functions don’t store the history of a variable.
See the following example:
// using normal variable
function App () {
const [temp, setTemp] = useState(0)
const count = 0; // re-sets on every render
console.log(count) // always 0
useEffect(() => {
count++;
}, [temp])
return (
<button onClick={() => setTemp(count+1)}>
</button>
)
}
// using useRef
function App () {
const [temp, setTemp] = useState(0)
const count = useRef(0);
console.log(count.current) // 0, 1, 2, ....
useEffect(() => {
count.current++;
}, [temp])
return (
<button onClick={() => setTemp(count+1)}>
</button>
)
}
Coding Problem
Problem statement: Write a code for a function that can be executed like sum(8)(5)()()(1)()(2)…
. The return value should be the sum of all the arguments passed in.
Solution: I declared a function called sum
to store the value of the total and returned another function from the sum
function so that it can be called recursively.
function sum (total = 0) {
return function inner (num = 0) {
total += num
return inner
}
}
console.log(sum(8)(5)()()(1)()(2))
The above code logs the actual inner
function and not the total
value as we don’t know the stopping condition here, so we always return the function.
Follow up: As we don’t know the stopping condition, update the code to get the value of total
using .get
on the function like sum(8)(5)()()(1)()(2).get()
.
Solution: As functions are also objects in JavaScript, I assigned the get property to the inner
function.
function sum (total = 0) {
function inner (num = 0) {
total += num
return inner
}
inner.get = function () {
return total
}
return inner
}
console.log(sum(8)(5)()()(1)()(2).get()) // 16
Conclusion
These were the interesting problems I felt like sharing with you. I hope these help you in your interviews!
That’s all folks! See you at the next one :)
Find more such content at SliceOfDev.
Please follow me on Twitter 🐤.