This repository contains a few hundred curated JavaScript & React interview questions with high quality answers for acing your Front End Engineer interviews.
In JavaScript, let, var, and const are all keywords used to declare variables, but they differ significantly in terms of scope, initialization rules, whether they can be redeclared or reassigned and the behavior when they are accessed before declaration:
| Behavior | var |
let |
const |
|---|---|---|---|
| Scope | Function or Global | Block | Block |
| Initialization | Optional | Optional | Required |
| Redeclaration | Yes | No | No |
| Reassignment | Yes | Yes | No |
| Accessing before declaration | undefined |
ReferenceError |
ReferenceError |
== is the abstract equality operator while === is the strict equality operator. The == operator will compare for equality after doing any necessary type conversions. The === operator will not do type conversion, so if two values are not the same type === will simply return false.
| Operator | == |
=== |
|---|---|---|
| Name | (Loose) Equality operator | Strict equality operator |
| Type coercion | Yes | No |
| Compares value and type | No | Yes |
Event Bubbling is a concept in the DOM (Document Object Model). It happens when an element receives an event, and that event bubbles up (or you can say is transmitted or propagated) to its parent and ancestor elements in the DOM tree until it gets to the root element.
- Event bubbling is essential for event delegation, where a single event handler manages events for multiple child elements, enhancing performance and code simplicity. While convenient, failing to manage event propagation properly can lead to unintended behavior, such as multiple handlers firing for a single event.
- Use event.stopPropagation() to stop the event bubbling to the parent / root elements
const div = document.getElementById("div");
const span = document.getElementById("span");
const button = document.getElementById("button");
div.addEventListener("click", () => {
console.log("div was clicked");
});
span.addEventListener("click", () => {
console.log("span was clicked");
});
button.addEventListener("click", (event) => {
// Use stopPropagation() to stop event bubbling
event.stopPropagation();
console.log("button was clicked");
});Event delegation is a event handling pattern in which you handle the events at a higher level in the DOM tree instead of the actual level where the event was received. The event delegation is based on event bubbling concept.
Advantages -
- Improved performance: Attaching a single event listener is more efficient than attaching multiple event listeners to individual elements, especially for large or dynamic lists. This reduces memory usage and improves overall performance.
- Dynamic Content Handling: Event delegation can automatically handle events on new child elements that are added later.
<div id="div">
<span id="span">
<button>button1</button>
<button>button2</button>
<button>button3</button>
<!-- Elements can be added without thinking much about the backend JS function -->
<button>button4</button>
</span>
</div>const div = document.getElementById("div");
div.addEventListener("click", (event) => {
const target = event.target;
if (target.tagName === "BUTTON") {
console.log(target.innerHTML);
}
});There's no simple explanation for this; it is one of the most confusing concepts in JavaScript because it's behavior differs from many other programming languages. The one-liner explanation of the this keyword is that it is a dynamic reference to the context in which a function is executed.
A longer explanation follows is that this follows these rules:
- If the new keyword is used when calling the function, meaning the function was used as a function constructor, the this inside the function is the newly-created object instance.
- If this is used in a class constructor, the this inside the constructor is the newly-created object instance.
- If apply(), call(), or bind() is used to call/create a function, this inside the function is the object that is passed in as the argument.
- If a function is called as a method (e.g. obj.method())βββthis is the object that the function is a property of.
- If a function is invoked as a free function invocation, meaning it was invoked without any of the conditions present above, this is the global object. In the browser, the global object is the window object. If in strict mode ('use strict';), this will be undefined instead of the global object.
- If multiple of the above rules apply, the rule that is higher wins and will set the this value.
- If the function is an ES2015 arrow function, it ignores all the rules above and receives the this value of its surrounding scope at the time it is created.
- For an in-depth explanation, do check out Arnav Aggrawal's article on Medium & this is JS, Simplified.
- [this in JS, simplified] (https://www.youtube.com/watch?v=MgOK_DwJqTM)
Callback function is a function which is passed as an argument to another function. Using callback helps you to call a function from another function.
function log(value) {
console.log(value);
}
function findSum(num1, num2, print) {
const sum = num1 + num2;
print(sum);
}
findSum(20, 30, log);
// Example -
// window.addEventListener(event, callback function)The event loop is concept within the browser runtime environment regarding how asynchronous operations are executed within JavaScript engines. It works as such:
- The JavaScript engine starts executing scripts, placing synchronous operations on the call stack.
- When an asynchronous operation is encountered (e.g.,
setTimeout(), HTTP request), it is offloaded to the respective Web API or Node.js API to handle the operation in the background. - Once the asynchronous operation completes, its callback function is placed in the respective queues β task queues (also known as macrotask queues / callback queues) or microtask queues. We will refer to "task queue" as "macrotask queue" from here on to better differentiate from the microtask queue.
- The event loop continuously monitors the call stack and executes items on the call stack. If/when the call stack is empty:
- Microtask queue is processed. Microtasks include promise callbacks (
then,catch,finally),MutationObservercallbacks, and calls toqueueMicrotask(). The event loop takes the first callback from the microtask queue and pushes it to the call stack for execution. This repeats until the microtask queue is empty. - Macrotask queue is processed. Macrotasks include web APIs like
setTimeout(), HTTP requests, user interface event handlers like clicks, scrolls, etc. The event loop dequeues the first callback from the macrotask queue and pushes it onto the call stack for execution. However, after a macrotask queue callback is processed, the event loop does not proceed with the next macrotask yet! The event loop first checks the microtask queue. Checking the microtask queue is necessary as microtasks have higher priority than macrotask queue callbacks. The macrotask queue callback that was just executed could have added more microtasks!- If the microtask queue is non-empty, process them as per the previous step.
- If the microtask queue is empty, the next macrotask queue callback is processed. This repeats until the macrotask queue is empty.
- Microtask queue is processed. Microtasks include promise callbacks (
- This process continues indefinitely, allowing the JavaScript engine to handle both synchronous and asynchronous operations efficiently without blocking the call stack.
The following are resources explaining the event loop:
- JavaScript Visualized - Event Loop, Web APIs, (Micro)task Queue (2024): Lydia Hallie is a popular educator on JavaScript and this is the best recent videos explaining the event loop. There's also an accompanying blog post for those who prefer detailed text-based explanations.
- In the Loop (2018): Jake Archibald previously from the Chrome team provides a visual demonstration of the event loop during JSConf 2018, accounting for different types of tasks.
- What the heck is the event loop anyway? (2014): Philip Robert's gave this epic talk at JSConf 2014 and it is one of the most viewed JavaScript videos on YouTube.
- The arr.map method is one of the most useful and often used.
- It calls the function for each element of the array and returns the array of results.
// The syntax -
let result = arr.map(function(item, index, array) {
// returns the new value instead of item
});
Usage -
let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(item => item.length);
alert(lengths); // 5,7,6- The
findmethod looks for a single (first) element that makes the function return true. - If there may be many, we can use
arr.filter(fn). filterreturns an array of all matching elements
// The syntax -
let results = arr.filter(function(item, index, array) {
// if true item is pushed to results and the iteration continues
// returns empty array if nothing found
});
// Example -
let users = [
{id: 1, name: "John"},
{id: 2, name: "Pete"},
{id: 3, name: "Mary"}
];
// returns array of the first two users
let someUsers = users.filter(item => item.id < 3);
alert(someUsers.length); // 2- The method
arr.reduceused to calculate a single value based on the array. - The function is applied to all array elements one after another and βcarries onβ its result to the next call.
// The syntax is:
let value = arr.reduce(function(accumulator, item, index, array) {
// ...
}, [initial]);
// Example -
let arr = [1, 2, 3, 4, 5];
let result = arr.reduce((sum, current) => sum + current, 0);
alert(result); // 15- accumulator β is the result of the previous function call, equals initial the first time (if initial is provided).
- item β is the current array item.
- index β is its position.
- array β is the array.
For an in-depth explanation do check - Array methods in JS - javascript.info
- For detailed explanation for the polyfills of map, filter, reduce do check - Array Methods Polyfills
// Polyfill for map
Array.prototype.myMap = function (cb) {
let temp = [];
for (let i = 0; i < this.length; i++) {
temp.push(cb(this[i], i, this));
}
return temp;
};
const multiplyThree = nums.myMap((num, index, arr) => {
return num * 3;
});
console.log(multiplyThree);
// Polyfill for filter
Array.prototype.myFilter = function (cb) {
let temp = [];
for (let i = 0; i < this.length; i++) {
if (cb(this[i], i, this)) temp.push(this[i]);
}
return temp;
};
const moreThanTwo = nums.myFilter((num) => {
return num > 2;
});
console.log(moreThanTwo);
// Polyfill for reduce
Array.prototype.myReduce = function (cb, initialValue) {
let acc = initialValue;
for (let i = 0; i < this.length; i++) {
acc = acc ? cb(acc, this[i], i, this) : this[i];
}
return acc;
};
const sum = nums.myReduce((acc, curr, i, arr) => {
return acc + curr;
}, 0);
console.log(sum);- The first difference betweenΒ
map()Β andΒforEach()Β is the returning value. TheΒforEach()Β method returnsΒundefinedΒ andΒmap()Β returns a new array with the transformed elements. Even if they do the same job, the returning value remains different. - The second difference between these array methods is the fact thatΒ
map()Β is chainable. This means that you can attachΒreduce(),Βsort(),Βfilter()Β and so on after performing aΒmap()Β method on an array. That's something you can't do withΒforEach()Β because, as you might guess, it returnsΒundefined.
For an in-depth explanation do check - Main Differences Between forEach and map
MapΒ is a collection of keyed data items, just like anΒ Object. But the main difference is thatΒ MapΒ allows keys of any type. Methods and properties are:
new Map()Β β creates the map.map.set(key, value)Β β stores the value by the key.map.get(key)Β β returns the value by the key,ΒundefinedΒ ifΒkeyΒ doesnβt exist in map.map.has(key)Β β returnsΒtrueΒ if theΒkeyΒ exists,ΒfalseΒ otherwise.map.delete(key)Β β removes the element (the key/value pair) by the key.map.clear()Β β removes everything from the map.map.sizeΒ β returns the current element count.
AΒ SetΒ is a special type collection β βset of valuesβ (without keys), where each value may occur only once. Its main methods are:
new Set([iterable])Β β creates the set, and if anΒiterableΒ object is provided (usually an array), copies values from it into the set.set.add(value)Β β adds a value, returns the set itself.set.delete(value)Β β removes the value, returnsΒtrueΒ ifΒvalueΒ existed at the moment of the call, otherwiseΒfalse.set.has(value)Β β returnsΒtrueΒ if the value exists in the set, otherwiseΒfalse.set.clear()Β β removes everything from the set.set.sizeΒ β is the elements count.
| Map | WeakMap |
|---|---|
| A Map is an unordered list of key-value pairs where the key and the value can be of any type like string, boolean, number, etc. | In a Weak Map, every key can only be an object and function. It used to store weak object references. |
| Maps are iterable. | WeakMaps are not iterable. |
| Maps will keep everything even if you donβt use them. | WeakMaps holds the reference to the key, not the key itself. |
The garbage collector doesnβt remove a key pointer from Map and also doesnβt remove the key from memory. |
The garbage collector goes ahead and removes the key pointer from WeakMap and also removes the key from memory. WeakMap allows the garbage collector to do its task but not the Map. |
| Maps have some properties : .set, .get, .delete, .size, .has, .forEach, Iterators. | WeakMaps have some properties : .set, .get, .delete, .has. |
- Shallow Copy: Copies only the first-level properties. Nested objects remain
- Deep Copy : Creates an entirely new object, including nested objects.
| Method | Type | Best For | Drawbacks |
|---|---|---|---|
Spread Operator (...) |
Shallow | Simple objects | Nested objects are still referenced |
Object.assign() |
Shallow | Copying top-level properties | Same as spread operator |
structuredClone() |
Deep | Best performance, modern JS | No functions, circular refs |
JSON.parse(JSON.stringify()) |
Deep | Quick & easy deep copy | Loses functions, Date, and undefined |
Lodash (_.cloneDeep()) |
Deep | Handles all cases | Requires a library |
Prototypical inheritance in JavaScript is a way for objects to inherit properties and methods from other objects. Every JavaScript object has a special hidden property called [[Prototype]] (commonly accessed via __proto__ or using Object.getPrototypeOf()) that is a reference to another object, which is called the object's "prototype".
When a property is accessed on an object and if the property is not found on that object, the JavaScript engine looks at the object's __proto__, and the __proto__'s __proto__ and so on, until it finds the property defined on one of the __proto__s or until it reaches the end of the prototype chain.
This behavior simulates classical inheritance, but it is really more of delegation than inheritance.
Here's an example of prototypal inheritance:
// Parent object constructor.
function Animal(name) {
this.name = name;
}
// Add a method to the parent object's prototype.
Animal.prototype.makeSound = function () {
console.log('The ' + this.constructor.name + ' makes a sound.');
};
// Child object constructor.
function Dog(name) {
Animal.call(this, name); // Call the parent constructor.
}
// Set the child object's prototype to be the parent's prototype.
Object.setPrototypeOf(Dog.prototype, Animal.prototype);
// Add a method to the child object's prototype.
Dog.prototype.bark = function () {
console.log('Woof!');
};
// Create a new instance of Dog.
const bolt = new Dog('Bolt');
// Call methods on the child object.
console.log(bolt.name); // "Bolt"
bolt.makeSound(); // "The Dog makes a sound."
bolt.bark(); // "Woof!"Things to note are:
.makeSoundis not defined onDog, so the JavaScript engine goes up the prototype chain and finds.makeSoundon the inheritedAnimal.- Using
Object.create()to build the inheritance chain is no longer recommended. UseObject.setPrototypeOf()instead.
In the book "You Don't Know JS" (YDKJS) by Kyle Simpson, a closure is defined as follows:
Closure is when a function is able to remember and access its lexical scope even when that function is executing outside its lexical scope
In simple terms, functions have access to variables that were in their scope at the time of their creation. This is what we call the function's lexical scope. A closure is a function that retains access to these variables even after the outer function has finished executing. This is like the function has a memory of its original environment.
function outerFunction() {
const outerVar = 'I am outside of innerFunction';
function innerFunction() {
console.log(outerVar); // `innerFunction` can still access `outerVar`.
}
return innerFunction;
}
const inner = outerFunction(); // `inner` now holds a reference to `innerFunction`.
inner(); // "I am outside of innerFunction"
// Even though `outerFunction` has completed execution, `inner` still has access to variables defined inside `outerFunction`.Key points to remember:
- Closure occurs when an inner function has access to variables in its outer (lexical) scope, even when the outer function has finished executing.
- Closure allows a function to remember the environment in which it was created, even if that environment is no longer present.
- Closures are used extensively in JavaScript, such as in callbacks, event handlers, and asynchronous functions.
| Trait | null |
undefined |
Undeclared |
|---|---|---|---|
| Meaning | Explicitly set by the developer to indicate that a variable has no value | Variable has been declared but not assigned a value | Variable has not been declared at all |
| Type | object |
undefined |
Throws a ReferenceError |
| Equality Comparison | null == undefined is true |
undefined == null is true |
Throws a ReferenceError |
JavaScript bind() method is a powerful tool that allows you to create a new function with a specifiedΒ thisΒ value and optional initial arguments. This method is essential for managing function context and creating partially applied functions in JavaScript.
// Bind Call - fn.bind(context, additional arguments)
// Bind Example -
let user = {
firstname: "subham",
};
function print() {
console.log(this.firstname);
}
let myFunc = print.bind(user);
myFunc();// Polyfill for Bind -
let user = {
firstname: "subham",
};
function printname(lastname) {
console.log(`${this.firstname} - ${lastname}`);
}
// bind - pollyfill using apply()
Function.prototype.bindUsingApply = function (ctx, ...args) {
let fn = this;
let allArgs = args;
return function (...newArgs) {
allArgs = [...allArgs, ...newArgs];
return fn.apply(ctx, allArgs);
};
};
// bind - polyfill without using apply()
Function.prototype.bindWithoutUsingApply = function(ctx, ...args){
ctx.callableFn = this;
let allArgs = args;
return function (...newArgs) {
allArgs = [...allArgs, ...newArgs];
return ctx.callableFn(allArgs);
};
}
let myName1 = printname.bindUsingApply(user, "123");
let myName = printname.bindWithoutUsingApply(user, "mohanty");
myName();
myName1();.call and .apply are both used to invoke functions with a specific this context and arguments. The primary difference lies in how they accept arguments:
.call(thisArg, arg1, arg2, ...): Takes arguments individually..apply(thisArg, [argsArray]): Takes arguments as an array.
Assuming we have a function add, the function can be invoked using .call and .apply in the following manner:
function add(a, b) {
return a + b;
}
console.log(add.call(null, 1, 2)); // 3
console.log(add.apply(null, [1, 2])); // 3let car1 = {
color: 'Red',
company: 'Ferrari',
};
let car2 = {
color: 'Blue',
company: 'BMW',
};
let car3 = {
color: 'White',
company: 'Mercedes',
};
function purchaseCar(currency, price) {
console.log(
`I have purchased ${this.color} - ${this.company} car for ${currency}${price} `
);
};
// Polyfill for bind()
Function.prototype.myBind = function (currentContext = {}, ...arg) {
if (typeof this !== 'function') {
throw new Error(this + "cannot be bound as it's not callable");
}
currentContext.fn = this;
return function () {
return currentContext.fn(...arg);
};
};
// polyfill for apply
Function.prototype.myApply = function (currentContext = {}, arg = []) {
if (typeof this !== 'function') {
throw new Error(this + "it's not callable");
}
if (!Array.isArray(arg)) {
throw new TypeError('CreateListFromArrayLike called on non-object')
}
currentContext.fn = this;
currentContext.fn(...arg);
};
// polyfill for call
Function.prototype.myCall = function (currentContext = {}, ...arg) {
if (typeof this !== 'function') {
throw new Error(this + "it's not callable");
}
currentContext.fn = this;
currentContext.fn(...arg);
};
const initPurchaseBmw = purchaseCar.myBind(car1, 'βΉ', '1,00,00,000');
initPurchaseBmw();
purchaseCar.myApply(car2, ['βΉ', '50,00,000']);
purchaseCar.myCall(car3, 'βΉ', '60,00,000');- Variable declarations (
var): Declarations are hoisted, but not initializations. The value of the variable isundefinedif accessed before initialization. - Variable declarations (
letandconst): Declarations are hoisted, but not initialized. Accessing them results inReferenceErroruntil the actual declaration is encountered. - Function expressions (
var): Declarations are hoisted, but not initializations. The value of the variable isundefinedif accessed before initialization. - Function declarations (
function): Both declaration and definition are fully hoisted. - Class declarations (
class): Declarations are hoisted, but not initialized. Accessing them results inReferenceErroruntil the actual declaration is encountered. - Import declarations (
import): Declarations are hoisted, and side effects of importing the module are executed before the rest of the code.
The following behavior summarizes the result of accessing the variables before they are declared.
| Declaration | Accessing before declaration |
|---|---|
var foo |
undefined |
let foo |
ReferenceError |
const foo |
ReferenceError |
class Foo |
ReferenceError |
var foo = function() { ... } |
undefined |
function foo() { ... } |
Normal |
import |
Normal |
function sayHi() {
alert(phrase); // alerts undefined instead of ReferenceError
var phrase = "Hello";
}
sayHi();
// The above code gets hoisted and becomes:
function sayHi() {
var phrase; // declaration works at the start...
alert(phrase); // undefined
phrase = "Hello"; // ...assignment - when the execution reaches it.
}
sayHi();NOTE: Declarations made with theΒ letΒ andΒ constΒ keywords are also subject to hoisting (i.e. they are moved to the top of theirΒ respective scope (global or block))Β but are said to be in aΒ temporal dead zone (TDZ)Β meaning that any attempt to access them will result in aΒ reference error.
TheΒ asyncΒ keyword before a function has two effects:
- Makes it always return a promise.
- AllowsΒ
awaitΒ to be used in it.
TheΒ awaitΒ keyword before a promise makes JavaScript wait until that promise settles, and then:
- If itβs an error, an exception is generated β same as ifΒ
throw errorΒ were called at that very place. - Otherwise, it returns the result.
Together they provide a great framework to write asynchronous code that is easy to both read and write.
WithΒ async/awaitΒ we rarely need to writeΒ promise.then/catch, but we still shouldnβt forget that they are based on promises, because sometimes (e.g. in the outermost scope) we have to use these methods. AlsoΒ Promise.allΒ is nice when we are waiting for many tasks simultaneously.
The virtual DOM is an in-memory representation of the real DOM elements. Instead of interacting directly with the real DOM, which can be slow and costly in terms of performance, React creates a virtual representation of the UI components. This virtual representation is a lightweight JavaScript object that mirrors the structure of the real DOM.
Here's a step-by-step process of how the virtual DOM works:
- Step 1 β Initial Rendering: when the app starts, the entire UI is represented as a Virtual DOM. React elements are created and rendered into the virtual structure.
- Step 2 β State and Props Changes: as the states and props change in the app, React re-renders the affected components in the virtual DOM. These changes do not immediately impact the real DOM.
- Step 3 β Comparison Using Diff Algorithm: React then uses aΒ diffing algorithmΒ to compare the current version of the Virtual DOM with the previous version. This process identifies the differences (or "diffs") between the two versions.
- Step 4 β Reconciliation Process: based on the differences identified, React determines the most efficient way to update the real DOM. Only the parts of the real DOM that need to be updated are changed, rather than re-rendering the entire UI. This selective updating is quick and performant.
- Step 5 β Update to the Real DOM: finally, React applies the necessary changes to the real DOM. This might involve adding, removing, or updating elements based on the differences detected in step 3.
Read the following article for detailed understanding - What is the Virtual DOM in React?
In React, reconciliation isΒ the process of updating the user interface (UI) when the state or data of a component changes.Β It's a core feature of React that helps ensure fast and efficient updates.Β
- React compares the current state of a component to its previous state
- React uses a diffing algorithm to identify the differences between the two states
- React determines which parts of the DOM need to be updated, added, or removed
- React updates the DOM to reflect the changes
Benefits of reconciliation
- Reconciliation minimizes the number of DOM operations, which improves performance
- Reconciliation ensures that the UI is consistent with the underlying data, which prevents rendering errors
React Fiber is a core algorithm within React that significantly improves rendering performance by breaking down the rendering process into smaller, manageable chunks, allowing React to pause and resume work as needed, thus maintaining responsiveness even during complex updates and preventing the UI from freezing during heavy computations; essentially enabling "incremental rendering" where updates are spread across multiple frames instead of happening all at once.
Key points about React Fiber:
- Incremental Rendering: The primary benefit of Fiber is its ability to split rendering work into smaller units called "fibers," allowing React to prioritize updates and distribute rendering across multiple frames, resulting in a smoother user experience.
- Priority-Based Updates: Developers can assign priorities to different updates, ensuring that critical changes are rendered first, while less important updates can be deferred.
- Pause and Resume Capability: React can pause rendering work in the middle of an update if a higher priority task comes in, and then resume later where it left off.
- Better Animation Handling: By enabling incremental rendering, Fiber is particularly beneficial for animations and gestures, allowing for smoother visual transitions.
How it works:
- Fiber Tree: When a component renders, React creates a tree-like structure called a "Fiber tree" where each node represents a component and its properties.
- Reconciliation: When data changes, React compares the new component tree with the existing one to identify differences and determine what needs to be updated in the DOM.
- Work Units: During reconciliation, the work is divided into smaller units (fibers) which can be paused and resumed depending on the priority and available time.
Overall, React Fiber significantly enhances the performance of React applications by allowing for more granular control over the rendering process, making it especially beneficial for complex UIs with frequent updates and animations.
Lifting state up is an important pattern for React developers because sometimes we have state that's located within a particular component that also needs to be shared with sibling components.
Instead of using an entire state management library like Redux or React Context, we can just lift the state up to the closest common ancestor and pass both the state variables the state values down as well as any callbacks to update that state.
Read the following article for detailed understanding - What Is "Lifting State Up" in React?
HOCs are functions that wrap existing components, providing them with additional props or behaviors. Like a gift wrap, wrapping an existing component and adding additional feature to the gift.
The main benefit of HOCs is that they enable us to extend the functionality of multiple components without repeating the same code in each of them. This promotes code reuse and enhances the maintainability of your React applications. Examples - Auth Check, Dark Mode / Light Mode Application etc.
Read the following article for detailed understanding - Mastering Higher Order Components (HOCs) in React | HackerNoon
TheΒ useStateΒ hook is perhaps the most basic and essential hook in React. It enables you to add state to your functional components, allowing them to keep track of data that changes over time. Let's dive into howΒ useStateΒ works with a simple example.
import React, { useState } from 'react';
const Counter = () => {
// Declare a state variable named 'count' with an initial value of 0
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
};
export default Counter;TheΒ useEffectΒ hook is used to perform side effects in your functional components, such as fetching data, subscribing to external events, or manually changing the DOM. It combines the functionality ofΒ componentDidMount,Β componentDidUpdate, andΒ componentWillUnmountΒ in class components.
import React, { useState, useEffect } from 'react';
const DataFetcher = () => {
const [data, setData] = useState(null);
useEffect(() => {
// Fetch data from an API
fetch('https://api.example.com/data')
.then((response) => response.json())
.then((result) => setData(result))
.catch((error) => console.error('Error fetching data:', error));
}, []); // Empty dependency array means this effect runs once after the initial render
return (
<div>
{data ? (
<ul>
{data.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
) : (
<p>Loading data...</p>
)}
</div>
);
};
export default DataFetcher;Cleanup using useEffect
Sometimes, side effects need to be cleaned up, especially when dealing with subscriptions or timers to prevent memory leaks. TheΒ useEffectΒ hook can return a cleanup function that will be executed when the component unmounts.
// In this example, the setInterval function is used to update the seconds state every second.
// The cleanup function returned by useEffect clears the interval when the component is unmounted.
import React, { useState, useEffect } from 'react';
const Timer = () => {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setSeconds((prevSeconds) => prevSeconds + 1);
}, 1000);
// Cleanup function to clear the interval when the component unmounts
return () => clearInterval(intervalId);
}, []); // Empty dependency array for initial setup only
return <p>Seconds: {seconds}</p>;
};
export default Timer;TheΒ useContextΒ hook is used to consume values from a React context. Context provides a way to pass data through the component tree without having to pass props manually at every level. Let's explore howΒ useContextΒ works with a simple example.
// We create an AuthContext using createContext and provide an AuthProvider component. The AuthProvider component wraps its children with the context provider and includes functions for logging in and out.
import React, { createContext, useContext, useState } from 'react';
const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const login = () => {
setIsAuthenticated(true);
};
const logout = () => {
setIsAuthenticated(false);
};
return (
<AuthContext.Provider value={{ isAuthenticated, login, logout }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
return useContext(AuthContext);
};// Consuming useContext in the children components
// Here, the useAuth hook is used to access the values provided by the AuthContext. The AuthStatus component displays the user's login status and provides buttons to log in and out.
import React from 'react';
import { useAuth } from './AuthContext';
const AuthStatus = () => {
const { isAuthenticated, login, logout } = useAuth();
return (
<div>
<p>User is {isAuthenticated ? 'logged in' : 'logged out'}</p>
<button onClick={login}>Login</button>
<button onClick={logout}>Logout</button>
</div>
);
};
export default AuthStatus;The useReducer hook in React is an alternative to useState for managing more complex state logic. It is particularly useful when the state depends on previous states or involves multiple sub-values, enabling better organization and control.
How useReducer Works
useReducer is based on the Reducer Pattern:
- Reducer Function: A pure function that takes the current state and an action, then returns the new state.
- Dispatch: A function used to send actions to the reducer.
- State: The state managed by the reducer.
Read the following article for detailed understanding - How to useReducer in React
Read the following article for detailed understanding - Custom Hooks in React
const useBoolean = () => {
const [state, setState] = React.useState();
const handleTrue = () => setState(true);
const handleFalse = () => setState(false);
const handleToggle = () => setState(!state);
return [
state,
{
setTrue: handleTrue,
setFalse: handleFalse,
setToggle: handleToggle,
},
];
};function App() {
const [isToggle, {
setToggle,
setTrue,
setFalse,
}] = useBoolean(false);
return (
<div>
<button type="button" onClick={setToggle}>
Toggle
</button>
<button type="button" onClick={setTrue}>
To True
</button>
<button type="button" onClick={setFalse}>
To False
</button>
{isToggle.toString()}
</div>
);
}To update the state of a parent component from a child component, you can pass a state-updating function (defined in the parent) as a prop to the child component. The child can then invoke this function to update the parent's state.
Steps to Update Parent State from Child
- Define State in the Parent Component: The parent component owns the state and provides a function to update it.
- Pass the Update Function as a Prop: The parent's state-updating function is passed to the child component as a prop.
- Invoke the Update Function in the Child: The child component calls the function to modify the parent's state. Example
Parent Component
import React, { useState } from "react";
import Child from "./Child";
const Parent = () => {
const [message, setMessage] = useState("Hello from Parent");
// Function to update state
const updateMessage = (newMessage) => {
setMessage(newMessage);
};
return (
<div>
<h1>{message}</h1>
<Child updateMessage={updateMessage} />
</div>
);
};
export default Parent;Child Component
import React from "react";
const Child = ({ updateMessage }) => {
const handleChange = () => {
updateMessage("Message updated from Child!");
};
return (
<button onClick={handleChange}>Update Parent Message</button>
);
};
export default Child;Alternative Approaches
- Using Context API:
- State Management Libraries
Prop drilling refers to the process of passing data (props) from a parent component to a deeply nested child component through multiple intermediary components, even if those intermediary components do not directly need the data.
Problems with Prop Drilling
- Unnecessary Complexity: Makes components tightly coupled, harder to maintain and refactor.
- Code Duplication: Repetitive passing of props through intermediate components.
- Scalability Issues: As the app grows, managing deeply nested props becomes cumbersome.
How to Avoid Prop Drilling -
- Use React Context API
- Use State Management Libraries - Redux, Zustand, MobX
- Higher Order components
- Custom Hooks
To call a parent component's method from a child component in React, you can pass the parent method as a prop to the child component. This establishes communication between the child and the parent.
React provides lazy loading capabilities using React.lazy() and React.Suspense to improve performance by dynamically loading components only when needed.
React.lazy()
React.lazy() is a function that enables code-splitting by loading components dynamically. It works with ES6βs dynamic import() to load a component only when it's needed.
Example: Using React.lazy()
import React, { Suspense, lazy } from "react";
// Lazy load the component
const LazyComponent = lazy(() => import("./LazyComponent"));
function App() {
return (
<div>
<h1>My App</h1>
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
</div>
);
}
export default App;π‘ How it Works:
lazy(() => import('./LazyComponent'))dynamically imports the component.- The component is only loaded when needed, reducing the initial bundle size.
- Must be wrapped in
<Suspense>to provide a fallback UI while loading.
React Suspense
React.Suspense is a component that lets you handle loading states for lazy-loaded components and asynchronous data fetching (in React Server Components).
Use Cases of Suspense
- Lazy Loading Components (via
React.lazy()) - Fetching Data with Suspense-enabled Libraries (like React Server Components, Relay, or React Query)
Example: Suspense for Data Fetching (React Server Components)
import { Suspense } from "react";
import UserProfile from "./UserProfile";
function App() {
return (
<Suspense fallback={<div>Loading user data...</div>}>
<UserProfile />
</Suspense>
);
}π‘ How it Works:
- When
UserProfileneeds to fetch data, React pauses rendering. - The fallback UI (
Loading user data...) is shown until the data is available.
Key Differences Between React.lazy() and React.Suspense
| Feature | React.lazy() | React.Suspense |
|---|---|---|
| Purpose | Code-splitting (dynamic component import) | Handles loading states for lazy components & async data |
| Works With | Components only | Components & data fetching |
| Needs Suspense? | Yes β | Yes β |
| Example Use Case | Lazy loading a Dashboard component | Showing a loading spinner while fetching user data |
π When to Use Them?
- Use
React.lazy()when you want to split your bundle and load components only when needed. - Use
React.Suspenseto handle loading states for lazy components or asynchronous data fetching in React Server Components.
Lazy loading is a design pattern used to improve application performance by deferring the loading of components or resources until they are actually needed. In React, lazy loading is commonly used for components to reduce the initial load time by splitting the code into smaller chunks (code-splitting).
How It Works When a React app is built, all components are usually bundled into a single JavaScript file. Lazy loading splits this file into smaller chunks, loading only the necessary parts as the user navigates the app. This helps in:
- Reducing initial load time.
- Improving user experience for larger applications.
- Optimizing resource utilization.
Syntax
const LazyComponent = React.lazy(() => import('./LazyComponent'));Example
Component Setup
// LazyComponent.js
import React from 'react';
const LazyComponent = () => {
return <h1>This is a lazy-loaded component!</h1>;
};
export default LazyComponent;Using Lazy Loading in the App
import React, { Suspense } from 'react';
// Lazy load the component
const LazyComponent = React.lazy(() => import('./LazyComponent'));
const App = () => {
return (
<div>
<h1>Welcome to My App</h1>
{/* Suspense provides a fallback UI while loading */}
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
</div>
);
};
export default App;Key Elements
-
React.lazy: Dynamically imports the component. -
Suspense: Wraps the lazy-loaded component and displays a fallback UI (like a loading spinner) while the component is being fetched.
Benefits of Lazy Loading
- Performance Optimization:
- Efficient Resource Utilization:
- Improved User Experience:
Read the following article to understand Lazy Loading in Routes
The main difference between useMemo and useCallback is that useMemo returns a value, while useCallback returns a function. Both are React hooks that help optimize performance by avoiding unnecessary re-renders.
| useMemo | useCallback | |
|---|---|---|
| What it returns | A memoized value | A memoized function |
| When it's used | For expensive calculations or data transformations | To cache a function that relies on changing props or state |
When to use
- useMemo is good for optimizing expensive calculations or data transformations.
- useCallback is good for handling events and other functions that get passed down to child components.
In React, form inputs can be handled in two ways: controlled and uncontrolled components. The key difference is who manages the form stateβReact or the DOM.
1οΈβ£ Controlled Components (React Manages State)
β
In controlled components, React controls the form element's value via useState.
β
The input value is always updated via state, and React re-renders when it changes.
πΉ Example: A controlled input field
import { useState } from "react";
function ControlledInput() {
const [name, setName] = useState("");
return (
<div>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<p>Typed: {name}</p>
</div>
);
}2οΈβ£ Uncontrolled Components (DOM Manages State)
β
In uncontrolled components, the DOM itself keeps track of the inputβs value.
β
Instead of using useState, we use useRef to access the input value only when needed.
πΉ Example: An uncontrolled input field
import { useRef } from "react";
function UncontrolledInput() {
const inputRef = useRef(null);
const handleSubmit = () => {
alert("Entered Name: " + inputRef.current.value);
};
return (
<div>
<input type="text" ref={inputRef} />
<button onClick={handleSubmit}>Submit</button>
</div>
);
}3οΈβ£ Key Differences:
| Feature | Controlled Components | Uncontrolled Components |
|---|---|---|
| State Management | Managed by React state (useState) |
Managed by DOM (useRef) |
| Re-renders | Re-renders on every change | No re-renders on change |
| Value Handling | value prop + onChange handler |
Access value via useRef |
| Performance | Can cause more re-renders | More performant (no re-renders) |
| Validation | Easier to validate in real-time | Harder, requires manual handling |
React provides three hooks for side effects:
useEffectβ Runs after the render is committed to the screen.useLayoutEffectβ Runs synchronously before the browser paints the screen.useInsertionEffectβ Runs before layout effects and is mainly for injecting styles.
| Hook | When It Runs | Blocks Rendering? | Use Case |
|---|---|---|---|
useEffect |
After render & paint | β No | Fetching data, subscriptions, timers |
useLayoutEffect |
After DOM mutation, before paint | β Yes | Measuring DOM, animations, style adjustments |
useInsertionEffect |
Before layout effects, before paint | β Yes | Injecting styles (CSS-in-JS) |
React Server Components (RSC) are a new type of React component that runs on the server, allowing faster page loads, reduced JavaScript bundle size, and better performance.
1. What Are React Server Components (RSC)?
- They run on the server instead of the browser.
- They return serialized UI (HTML/JSON) instead of JavaScript.
- They donβt include client-side JavaScript, making them lightweight.
- They fetch data on the server without exposing API calls to the client.
2. What Are Client Components?
- Traditional React components that run in the browser.
- They can use state (
useState) and effects (useEffect). - They execute on the client, meaning more JavaScript is sent to the browser.
| Feature | Server Components (RSC) | Client Components |
|---|---|---|
| Where They Run | Server (before HTML is sent) | Browser (on the client) |
| JavaScript on Client? | β No JavaScript sent | β JavaScript is sent |
| State & Effects | β Cannot use state (useState) or effects (useEffect) |
β Can use state, hooks, and effects |
| Data Fetching | β Fetches data directly on the server | β Fetches data via API calls |
| Performance | β Less JavaScript β Faster load times | β More JavaScript β Potential performance hit |
| Interactivity | β Not interactive | β Can handle user interactions |
β Creating a Server Component (Default in Next.js)
// Server Component (automatically runs on the server)
export default async function UserProfile({ userId }) {
const user = await fetch(`https://api.example.com/users/${userId}`).then(res => res.json());
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}β
Creating a Client Component
If you need state, effects, or interactivity, add "use client" at the top.
"use client";
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}| Use Case | Use Server Component? | Use Client Component? |
|---|---|---|
| Static content (e.g., blog posts, product lists) | β Yes | β No |
| Data fetching (e.g., fetching from a database) | β Yes | β No |
| Forms, user input, interactivity (e.g., buttons, modals) | β No | β Yes |
Using state (useState, useReducer) |
β No | β Yes |
Effects (useEffect, event listeners) |
β No | β Yes |