Slice of Dev Logo

React Interview Experience

Cover Image for React Interview Experience
Author's Profile Pic
Rajkumar Gaur

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 🐤.


Cover Image for Streams in NodeJS

Streams in NodeJS

Node.js Streams are an essential feature of the platform that provide an efficient way to handle data flows. They allow for processing of large volumes of data in a memory-efficient and scalable way, and can be used for a variety of purposes such as reading from and writing to files, transforming data, and combining multiple streams into a single pipeline

Author's Profile Pic
Rajkumar Gaur
Cover Image for Promises with Loops and Array Methods in JavaScript

Promises with Loops and Array Methods in JavaScript

Promises are objects that represent the eventual completion (or failure) of an asynchronous operation and allow you to handle the result of that operation when it completes. It’s important to understand how loops behave and how to use promises to control the flow of execution.

Author's Profile Pic
Rajkumar Gaur