practice for PW sde 1 frontend interview
##Different datatypes in JavaScript##
In JavaScript, data types are mainly divided into Primitive and Non-Primitive (Reference) types.
Primitive Types:
1. Number: Represents both integer and floating-point numbers.
2. String: Represents a sequence of characters.
3. Boolean: Represents true or false.
4. Undefined: Represents a variable that has been declared but not assigned a value.
5. Null: Represents the intentional absence of any object value.
6. Symbol: Represents a unique and immutable value that can be used as an object property key.
7. BigInt: Represents integers with arbitrary precision.
Non-Primitive (Reference) Types:
1. Object: Represents a collection of properties.
2. Array: Represents an ordered collection of values.
3. Function: Represents a callable object.
##Difference between var, let, and const##
VAR IN JAVASCRIPT
1. Function Scoped
Variables declared with var are function-scoped, not block-scoped.
This means:
- Accessible anywhere inside the function
- If declared outside a function, it becomes global
Example (Function Scope):
function test() {
var x = 10;
console.log(x); // 10
}
test();
console.log(x); // ReferenceError
Not Block Scoped:
var ignores block scope like if, for, and curly braces
if (true) {
var y = 20;
}
console.log(y); // 20 (accessible outside block)
This is a common interview question.
2. Hoisting
var declarations are hoisted to the top of their function scope.
Only the declaration is hoisted, not the initialization.
Example:
console.log(a); // undefined
var a = 5;
JavaScript internally treats it like:
var a; // hoisted
console.log(a); // undefined
a = 5;
That's why it prints undefined instead of throwing an error.
3. Can Be Re-declared
var allows redeclaration in the same scope.
var name = "Shamim";
var name = "Akhter";
console.log(name); // "Akhter"
This can cause bugs in large applications.
4. Adds Property to Global Object
If declared globally, var attaches to the window object in browser.
var age = 25;
console.log(window.age); // 25 (in browser)
But let and const do NOT do this.
5. Classic var Loop Problem (Interview Favorite)
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
Output:
3
3
3
Because:
- var is function-scoped
- All callbacks share the same i
- After loop ends, i equals 3
LET IN JAVASCRIPT
1. Block Scoped
Variables declared with let are block-scoped.
They are only accessible within the block they are declared in.
Example:
if (true) {
let x = 10;
console.log(x); // 10
}
console.log(x); // ReferenceError
2. Hoisting with Temporal Dead Zone
let is hoisted but not initialized.
Accessing it before declaration results in ReferenceError.
Example:
console.log(a); // ReferenceError
let a = 5;
This is called the Temporal Dead Zone (TDZ).
3. Cannot Be Re-declared
let does not allow redeclaration in the same scope.
let name = "Shamim";
let name = "Akhter"; // SyntaxError
4. Can Be Reassigned
let allows reassignment of values.
let age = 25;
age = 30; // Valid
console.log(age); // 30
5. Does Not Add to Global Object
Even if declared globally, let does not attach to window object.
let age = 25;
console.log(window.age); // undefined
CONST IN JAVASCRIPT
1. Block Scoped
Like let, const is also block-scoped.
Example:
if (true) {
const x = 10;
console.log(x); // 10
}
console.log(x); // ReferenceError
2. Hoisting with Temporal Dead Zone
const is hoisted but not initialized.
Accessing it before declaration results in ReferenceError.
Example:
console.log(a); // ReferenceError
const a = 5;
3. Cannot Be Re-declared
const does not allow redeclaration in the same scope.
const name = "Shamim";
const name = "Akhter"; // SyntaxError
4. Cannot Be Reassigned
const does not allow reassignment.
const age = 25;
age = 30; // TypeError: Assignment to constant variable
However, for objects and arrays, the properties can be modified:
const person = { name: "Shamim" };
person.name = "Akhter"; // Valid
person.age = 25; // Valid
const arr = [1, 2, 3];
arr.push(4); // Valid
arr = [5, 6]; // TypeError
5. Must Be Initialized
const must be initialized at the time of declaration.
const x; // SyntaxError: Missing initializer
const y = 10; // Valid
6. Does Not Add to Global Object
Even if declared globally, const does not attach to window object.
const age = 25;
console.log(window.age); // undefined
WHY var IS AVOIDED TODAY
Problems with var:
- No block scope
- Hoisting confusion
- Allows redeclaration
- Loop closure issues
Modern JavaScript prefers:
- let for variables that need to be reassigned
- const for variables that should not be reassigned
COMPARISON TABLE
Feature | var | let | const
---------------------------|------------------|------------------|------------------
Scope | Function | Block | Block
Hoisting | Yes (undefined) | Yes (TDZ) | Yes (TDZ)
Temporal Dead Zone | No | Yes | Yes
Re-declaration | Allowed | Not Allowed | Not Allowed
Re-assignment | Allowed | Allowed | Not Allowed
Initialization Required | No | No | Yes
Global Object Property | Yes | No | No
Best Use Case | Avoid | Mutable values | Immutable values
###Temporal Dead Zone###
Temporal Dead Zone (TDZ) is a behavior in JavaScript that occurs when you try to access a variable before it has been declared.
accessing a let or const variable before it's declared results in a ReferenceError.
for example:
console.log(x); // ReferenceError
let x = 5;
###Normal vs Arrow functions (syntax + this behavior)###
NORMAL FUNCTIONS
1. Syntax
Normal functions are declared using the function keyword.
They can be defined in multiple ways:
Function Declaration:
function greet(name) {
return "Hello " + name;
}
Function Expression:
const greet = function(name) {
return "Hello " + name;
};
Named Function Expression:
const greet = function greetUser(name) {
return "Hello " + name;
};
2. this Binding
Normal functions have their own this context.
The value of this depends on how the function is called.
Example (Object Method):
const person = {
name: "Shamim",
greet: function() {
console.log("Hello " + this.name);
}
};
person.greet(); // "Hello Shamim"
Example (Lost Context):
const person = {
name: "Shamim",
greet: function() {
console.log("Hello " + this.name);
}
};
const greetFunc = person.greet;
greetFunc(); // "Hello undefined" (this refers to global object)
3. arguments Object
Normal functions have access to the arguments object.
This is an array-like object containing all arguments passed to the function.
function sum() {
let total = 0;
for (let i = 0; i < arguments.length; i++) {
total += arguments[i];
}
return total;
}
sum(1, 2, 3, 4); // 10
4. Can Be Used as Constructor
Normal functions can be used with the new keyword to create objects.
function Person(name, age) {
this.name = name;
this.age = age;
}
const shamim = new Person("Shamim", 25);
5. Hoisting
Function declarations are fully hoisted.
You can call them before they are defined in the code.
greet("Shamim"); // Works fine
function greet(name) {
return "Hello " + name;
}
Function expressions are not hoisted:
greet("Shamim"); // ReferenceError
const greet = function(name) {
return "Hello " + name;
};
ARROW FUNCTIONS
1. Syntax
Arrow functions provide a shorter syntax using the arrow notation.
They were introduced in ES6.
Basic Syntax:
const greet = (name) => {
return "Hello " + name;
};
Implicit Return (single expression):(means no need to write return keyword)
const greet = (name) => "Hello " + name;
Single Parameter (parentheses optional):
const greet = name => "Hello " + name;
No Parameters:
const greet = () => "Hello World";
Multiple Parameters:
const add = (a, b) => a + b;
2. this Binding (Lexical this)
Arrow functions do NOT have their own this context.
They inherit this from the surrounding scope (lexical scoping).
Example (Object Method):
const person = {
name: "Shamim",
greet: () => {
console.log("Hello " + this.name);
}
};
person.greet(); // "Hello undefined" (this refers to outer scope, not person)
Example (Callback in Object Method):
const person = {
name: "Shamim",
hobbies: ["coding", "reading"],
printHobbies: function() {
this.hobbies.forEach((hobby) => {
console.log(this.name + " likes " + hobby);
});
}
};
person.printHobbies();
// "Shamim likes coding"
// "Shamim likes reading"
With normal function, this would fail:
const person = {
name: "Shamim",
hobbies: ["coding", "reading"],
printHobbies: function() {
this.hobbies.forEach(function(hobby) {
console.log(this.name + " likes " + hobby);
});
}
};
person.printHobbies();
// "undefined likes coding"
// "undefined likes reading"
3. No arguments Object
Arrow functions do NOT have their own arguments object.
Use rest parameters instead.
const sum = (...args) => {
return args.reduce((total, num) => total + num, 0);
};
sum(1, 2, 3, 4); // 10
4. Cannot Be Used as Constructor
Arrow functions cannot be used with the new keyword.
They will throw a TypeError.
const Person = (name, age) => {
this.name = name;
this.age = age;
};
const shamim = new Person("Shamim", 25); // TypeError: Person is not a constructor
5. No Hoisting
Arrow functions are not hoisted.
They must be defined before they are used.
greet("Shamim"); // ReferenceError
const greet = (name) => "Hello " + name;
6. Cannot Be Used as Methods
Arrow functions should not be used as object methods when you need this.
Bad Practice:
const person = {
name: "Shamim",
greet: () => {
console.log("Hello " + this.name); // this is not person
}
};
Good Practice:
const person = {
name: "Shamim",
greet() {
console.log("Hello " + this.name); // this is person
}
};
7. Implicit Return
Arrow functions can have implicit return for single expressions.
No need for return keyword or curly braces.
const square = (x) => x * x;
const getUser = () => ({ name: "Shamim", age: 25 });
Note: When returning an object literal, wrap it in parentheses.
WHEN TO USE NORMAL FUNCTIONS
Use normal functions when:
- You need dynamic this binding
- You need to use the arguments object
- You need to use the function as a constructor
- You are defining object methods
- You need function hoisting
WHEN TO USE ARROW FUNCTIONS
Use arrow functions when:
- You want to preserve the outer this context
- You are writing short, simple functions
- You are using callbacks (map, filter, forEach, etc.)
- You want cleaner syntax
- You do not need arguments object or constructor functionality
COMPARISON TABLE
Feature | Normal Function | Arrow Function
---------------------------|---------------------------|---------------------------
Syntax | function name() {} | () => {}
this Binding | Dynamic (call-site) | Lexical (outer scope)
arguments Object | Available | Not Available
Constructor | Can be used with new | Cannot be used with new
Hoisting | Yes (declarations) | No
Implicit Return | No | Yes (single expression)
Method Definition | Recommended | Not Recommended
Use in Callbacks | Works but verbose | Preferred
prototype Property | Yes | No
Best Use Case | Methods, Constructors | Callbacks, Short functions
###what is callback function in javascript###
A callback function is a function that is passed as an argument to another function and is executed after some operation has been completed.
example:
function sum(a, b, callback) {
const result = a + b;
callback(result);
}
sum(5, 3, (result) => {
console.log("The result is: " + result);
});
// Example using a callback function defined separately and passed as a parameter
const numbers = [1, 2, 3, 4, 5];
// Callback function to double a number
const doubleNumber = (num) => {
return num * 2;
};
// Using the map method with the callback function
const doubledNumbers = numbers.map(doubleNumber);
console.log(doubledNumbers); // Output: [2, 4, 6, 8, 10]
###Difference between map, reduce, and filter###
MAP
The map() method creates a new array by applying a given function to each element of the original array.
SYNTAX:
const newArray = arr.map((element, index, array) => {
// Return the new value for the element
});
example:
const numbers = [1, 2, 3, 4, 5];
const doubledNumbers = numbers.map((num) => num * 2);
console.log(doubledNumbers); // Output: [2, 4, 6, 8, 10]
FILTER
The filter() method creates a new array with all elements that pass the test implemented by the provided function.
SYNTAX:
const filteredArray = arr.filter((element, index, array) => {
// Return true to keep the element, false to exclude it
});
example:
const numbers = [1, 2, 3, 4, 5];
const evenNumbers = numbers.filter((num) => num % 2 === 0);
console.log(evenNumbers); // Output: [2, 4]
REDUCE
The reduce() method applies a function against an accumulator and each element in the array (from left to right) to reduce it to a single value.
SYNTAX:
const reducedValue = arr.reduce((accumulator, element, index, array) => {
// Return the new value for the accumulator
}, initialValue);
example:
const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce((acc, num) => acc + num, 0);
console.log(sum); // Output: 15
=====REACTJS=====
###Difference between props and state###
Props:
Props are read-only.
Props are immutable.
Props are used to pass data from parent to child components.
Props are used to configure a component.
Props are accessible via this.props in class components and props in functional components.
State:
State is mutable.
State is used to store data that can change over time.
State is used to store UI state.
State is accessible via this.state in class components and useState hook in functional components.
###Difference between state and local variable###
STATE IN REACT
1. Definition
State is a built-in React object used to store data that affects component rendering.
When state changes, the component re-renders automatically.
2. Mutability
State should be treated as immutable.
You cannot directly modify state; you must use setState or setter functions.
Class Component:
this.state = { count: 0 };
this.setState({ count: 1 }); // Correct
this.state.count = 1; // Wrong - Direct mutation
Functional Component:
const [count, setCount] = useState(0);
setCount(1); // Correct
count = 1; // Wrong - Direct mutation
3. Persistence
State persists across component re-renders.
The value is maintained throughout the component lifecycle.
Example:
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1); // State persists after re-render
};
return <button onClick={increment}>{count}</button>;
}
4. Triggers Re-render
Changing state triggers a component re-render.
React updates the UI automatically when state changes.
Example:
const [name, setName] = useState("Shamim");
setName("Akhter"); // Component re-renders with new name
5. Scope
State is accessible throughout the component.
Can be passed to child components as props.
Example:
function Parent() {
const [data, setData] = useState("Hello");
return <Child message={data} />;
}
6. Use Cases
- Storing form input values
- Tracking UI state (open/closed, visible/hidden)
- Managing data fetched from APIs
- Controlling conditional rendering
- Tracking user interactions
LOCAL VARIABLES
1. Definition
Local variables are regular JavaScript variables declared inside a function or block.
They follow standard JavaScript scoping rules.
2. Mutability
Local variables are mutable.
You can directly change their values.
Example:
function example() {
let count = 0;
count = 1; // Allowed - Direct mutation
count++; // Allowed
}
3. Persistence
Local variables do NOT persist across re-renders.
They are reset every time the function runs.
Example:
function Counter() {
let count = 0; // Reset to 0 on every render
const increment = () => {
count++; // This won't work as expected
console.log(count); // Shows updated value
};
return <button onClick={increment}>{count}</button>; // Always shows 0
}
4. Does NOT Trigger Re-render
Changing a local variable does NOT trigger a re-render.
The UI will not update when the variable changes.
Example:
function Example() {
let message = "Hello";
const changeMessage = () => {
message = "World"; // Variable changes but UI doesn't update
};
return <div>{message}</div>; // Always shows "Hello"
}
5. Scope
Local variables are accessible only within the function or block where they are defined.
They cannot be passed to child components (they reset on re-render).
Example:
function example() {
let x = 10; // Only accessible in this function
if (true) {
let y = 20; // Only accessible in this block
}
}
6. Use Cases
- Temporary calculations
- Loop counters
- Intermediate values in computations
- Data that doesn't affect rendering
- Helper variables for logic
KEY DIFFERENCES SUMMARY
State:
- Managed by React
- Triggers re-render when changed
- Persists across re-renders
- Should be treated as immutable
- Used for data that affects UI
- Accessible throughout component
Local Variables:
- Regular JavaScript variables
- Does NOT trigger re-render
- Reset on every re-render
- Can be directly mutated
- Used for temporary data
- Limited to function/block scope
COMPARISON TABLE
Feature | State | Local Variable
---------------------------|---------------------------|---------------------------
Managed By | React | JavaScript
Triggers Re-render | Yes | No
Persists Across Re-renders | Yes | No
Mutability | Immutable (use setter) | Mutable (direct change)
Scope | Component-wide | Function/Block scope
Use Case | UI-affecting data | Temporary calculations
Declaration | useState / this.state | let / const / var
Updates UI | Yes | No
Can Pass to Children | Yes (as props) | No (resets on re-render)
###React hooks usage###
WHAT ARE REACT HOOKS
Hooks are functions that let you use React features in functional components.
They were introduced in React 16.8 to allow state and lifecycle features without writing class components.
Hooks always start with the word "use" (useState, useEffect, useContext, etc.).
RULES OF HOOKS
1. Only Call Hooks at the Top Level
Do not call hooks inside loops, conditions, or nested functions.
Always use hooks at the top level of your React function.
Wrong:
if (condition) {
const [count, setCount] = useState(0); // Error
}
Correct:
const [count, setCount] = useState(0);
if (condition) {
// Use count here
}
2. Only Call Hooks from React Functions
Call hooks from React functional components or custom hooks.
Do not call hooks from regular JavaScript functions.
Wrong:
function regularFunction() {
const [count, setCount] = useState(0); // Error
}
Correct:
function MyComponent() {
const [count, setCount] = useState(0); // Correct
}
COMMONLY USED REACT HOOKS
1. useState
Purpose:
Adds state to functional components.
Returns an array with current state value and a function to update it.
Syntax:
const [state, setState] = useState(initialValue);
Example:
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
Multiple State Variables:
function Form() {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [age, setAge] = useState(0);
return (
<form>
<input value={name} onChange={(e) => setName(e.target.value)} />
<input value={email} onChange={(e) => setEmail(e.target.value)} />
<input value={age} onChange={(e) => setAge(e.target.value)} />
</form>
);
}
State with Objects:
function User() {
const [user, setUser] = useState({ name: "", age: 0 });
const updateName = (name) => {
setUser({ ...user, name }); // Spread to keep other properties
};
return <div>{user.name}</div>;
}
State with Arrays:
function TodoList() {
const [todos, setTodos] = useState([]);
const addTodo = (todo) => {
setTodos([...todos, todo]); // Add new item
};
const removeTodo = (index) => {
setTodos(todos.filter((_, i) => i !== index)); // Remove item
};
return <div>{todos.map((todo, i) => <p key={i}>{todo}</p>)}</div>;
}
2. useEffect
Purpose:
Performs side effects in functional components.
Replaces componentDidMount, componentDidUpdate, and componentWillUnmount.
Syntax:
useEffect(() => {
// Side effect code
return () => {
// Cleanup code (optional)
};
}, [dependencies]);
Example (Run on Every Render):
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log("Component rendered");
}); // No dependency array - runs on every render
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
Example (Run Once on Mount):
function Example() {
useEffect(() => {
console.log("Component mounted");
}, []); // Empty array - runs only once
return <div>Hello</div>;
}
Example (Run When Dependency Changes):
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log("Count changed:", count);
}, [count]); // Runs when count changes
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
Example (Cleanup Function):
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
return () => {
clearInterval(interval); // Cleanup on unmount
};
}, []);
return <div>Seconds: {seconds}</div>;
}
Example (Fetching Data):
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetch(`/api/users/${userId}`)
.then(response => response.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, [userId]); // Re-fetch when userId changes
if (loading) return <div>Loading...</div>;
return <div>{user.name}</div>;
}
3. useContext
Purpose:
Accesses context values without using Context.Consumer.
Simplifies consuming context in functional components.
Syntax:
const value = useContext(MyContext);
Example:
const ThemeContext = React.createContext("light");
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
return <ThemedButton />;
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button className={theme}>I am {theme}</button>;
}
4. useRef
Purpose:
Creates a mutable reference that persists across re-renders.
Does not trigger re-render when the value changes.
Commonly used to access DOM elements.
Syntax:
const refContainer = useRef(initialValue);
Example (Accessing DOM Elements):
function TextInput() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus();
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus Input</button>
</div>
);
}
Example (Storing Previous Value):
function Counter() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count;
});
const prevCount = prevCountRef.current;
return (
<div>
<p>Current: {count}, Previous: {prevCount}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Example (Storing Mutable Value):
function Timer() {
const [seconds, setSeconds] = useState(0);
const intervalRef = useRef(null);
const start = () => {
intervalRef.current = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
};
const stop = () => {
clearInterval(intervalRef.current);
};
return (
<div>
<p>Seconds: {seconds}</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}
5. useReducer
Purpose:
Alternative to useState for complex state logic.
Similar to Redux reducer pattern.
Useful when state depends on previous state or has multiple sub-values.
Syntax:
const [state, dispatch] = useReducer(reducer, initialState);
Example:
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
case "reset":
return { count: 0 };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "reset" })}>Reset</button>
</div>
);
}
6. useMemo
Purpose:
Memoizes expensive calculations.
Only re-computes when dependencies change.
Optimizes performance by avoiding unnecessary calculations.
Syntax:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
Example:
function ExpensiveComponent({ numbers }) {
const sum = useMemo(() => {
console.log("Calculating sum...");
return numbers.reduce((total, num) => total + num, 0);
}, [numbers]); // Only recalculate when numbers change
return <div>Sum: {sum}</div>;
}
7. useCallback
Purpose:
Memoizes callback functions.
Returns the same function reference unless dependencies change.
Prevents unnecessary re-renders of child components.
Syntax:
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
Example:
function Parent() {
const [count, setCount] = useState(0);
const [other, setOther] = useState(0);
const increment = useCallback(() => {
setCount(c => c + 1);
}, []); // Function reference stays the same
return (
<div>
<p>Count: {count}</p>
<p>Other: {other}</p>
<Child onIncrement={increment} />
<button onClick={() => setOther(other + 1)}>Change Other</button>
</div>
);
}
function Child({ onIncrement }) {
console.log("Child rendered");
return <button onClick={onIncrement}>Increment</button>;
}
CUSTOM HOOKS
Custom hooks are reusable functions that use built-in hooks.
They allow you to extract component logic into reusable functions.
Custom hooks must start with "use" prefix.
Example (useLocalStorage):
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
Usage:
function App() {
const [name, setName] = useLocalStorage("name", "");
return (
<input
value={name}
onChange={(e) => setName(e.target.value)}
/>
);
}
Example (useFetch):
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch(url)
.then(response => response.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(error => {
setError(error);
setLoading(false);
});
}, [url]);
return { data, loading, error };
}
Usage:
function UserList() {
const { data, loading, error } = useFetch("/api/users");
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{data.map(user => <p key={user.id}>{user.name}</p>)}</div>;
}
Example (useToggle):
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => {
setValue(v => !v);
}, []);
return [value, toggle];
}
Usage:
function Modal() {
const [isOpen, toggleOpen] = useToggle(false);
return (
<div>
<button onClick={toggleOpen}>Toggle Modal</button>
{isOpen && <div>Modal Content</div>}
</div>
);
}
HOOKS COMPARISON TABLE
Hook | Purpose | Returns | Triggers Re-render
--------------|-----------------------------------|----------------------|-------------------
useState | Manage component state | [state, setState] | Yes
useEffect | Handle side effects | Cleanup function | No
useContext | Access context values | Context value | Yes (when context changes)
useRef | Store mutable reference | Ref object | No
useReducer | Manage complex state | [state, dispatch] | Yes
useMemo | Memoize expensive calculations | Memoized value | No
useCallback | Memoize callback functions | Memoized function | No
WHEN TO USE EACH HOOK
useState:
- Simple state management
- Independent state values
- Form inputs, toggles, counters
useEffect:
- Data fetching
- Subscriptions
- DOM manipulation
- Timers and intervals
- Cleanup operations
useContext:
- Accessing global state
- Theme, authentication, language
- Avoiding prop drilling
useRef:
- Accessing DOM elements
- Storing mutable values that don't trigger re-render
- Keeping previous values
- Storing timer IDs
useReducer:
- Complex state logic
- Multiple sub-values
- State transitions depend on previous state
- Alternative to Redux for local state
useMemo:
- Expensive calculations
- Filtering or sorting large arrays
- Complex computations
- Preventing unnecessary recalculations
useCallback:
- Passing callbacks to optimized child components
- Preventing unnecessary re-renders
- Dependencies for other hooks
- Event handlers passed as props
COMMON MISTAKES TO AVOID
1. Calling Hooks Conditionally
Wrong:
if (condition) {
useState(0); // Error
}
Correct:
const [state, setState] = useState(0);
if (condition) {
setState(1);
}
2. Missing Dependencies in useEffect
Wrong:
useEffect(() => {
console.log(count);
}, []); // Missing count dependency
Correct:
useEffect(() => {
console.log(count);
}, [count]);
3. Not Cleaning Up in useEffect
Wrong:
useEffect(() => {
const interval = setInterval(() => {}, 1000);
}, []); // Memory leak
Correct:
useEffect(() => {
const interval = setInterval(() => {}, 1000);
return () => clearInterval(interval);
}, []);
4. Overusing useMemo and useCallback
These hooks have overhead. Only use them when:
- Calculations are truly expensive
- Preventing re-renders of child components
- Dependencies in other hooks
5. Mutating State Directly
Wrong:
const [user, setUser] = useState({ name: "Shamim" });
user.name = "Akhter"; // Direct mutation
Correct:
setUser({ ...user, name: "Akhter" });
BEST PRACTICES
- Keep hooks at the top level of your component
- Use descriptive names for custom hooks
- Extract reusable logic into custom hooks
- Keep useEffect focused on one concern
- Always include all dependencies in dependency arrays
- Use ESLint plugin for hooks to catch mistakes
- Prefer multiple useState calls over one complex state object
- Use useReducer for complex state logic
- Clean up side effects in useEffect return function
###Counter using useState and extending it with a custom hook to store previous value###
BASIC COUNTER WITH useState
Step 1: Simple Counter Component
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
const reset = () => {
setCount(0);
};
return (
<div>
<h2>Counter: {count}</h2>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
<button onClick={reset}>Reset</button>
</div>
);
}
Explanation:
- useState(0) initializes count to 0
- setCount updates the count value
- Each button click triggers the respective function
- Component re-renders when count changes
CREATING A CUSTOM HOOK TO STORE PREVIOUS VALUE
Step 2: usePrevious Custom Hook
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
How it works:
- useRef creates a mutable reference that persists across re-renders
- useEffect runs after every render and stores the current value
- On the next render, ref.current still holds the previous value
- The hook returns the previous value
Why this works:
1. Component renders with new value
2. usePrevious is called with new value
3. ref.current still contains old value (from previous render)
4. Hook returns old value
5. useEffect runs after render and updates ref.current to new value
6. Next render, the cycle repeats
EXTENDED COUNTER WITH PREVIOUS VALUE
Step 3: Counter with Previous Value Display
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
const reset = () => {
setCount(0);
};
return (
<div>
<h2>Current Count: {count}</h2>
<h3>Previous Count: {prevCount !== undefined ? prevCount : "N/A"}</h3>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
<button onClick={reset}>Reset</button>
</div>
);
}
Explanation:
- usePrevious(count) returns the previous value of count
- On first render, prevCount is undefined
- After first update, prevCount shows the previous value
- Display "N/A" when there is no previous value
COMPLETE IMPLEMENTATION
Step 4: Full Code with Both Hooks
import React, { useState, useEffect, useRef } from 'react';
Custom Hook:
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
Counter Component:
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
const reset = () => {
setCount(0);
};
const incrementByFive = () => {
setCount(count + 5);
};
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<h1>Counter with Previous Value</h1>
<div style={{ fontSize: '24px', margin: '20px' }}>
<p>Current Count: <strong>{count}</strong></p>
<p>Previous Count: <strong>{prevCount !== undefined ? prevCount : "N/A"}</strong></p>
{prevCount !== undefined && (
<p>Change: <strong>{count - prevCount}</strong></p>
)}
</div>
<div>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
<button onClick={incrementByFive}>+5</button>
<button onClick={reset}>Reset</button>
</div>
</div>
);
}
export default Counter;
ENHANCED VERSION WITH MORE FEATURES
Step 5: Advanced Counter with History
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
function Counter() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
const prevCount = usePrevious(count);
const prevStep = usePrevious(step);
const increment = () => {
setCount(count + step);
};
const decrement = () => {
setCount(count - step);
};
const reset = () => {
setCount(0);
};
const handleStepChange = (e) => {
setStep(Number(e.target.value));
};
return (
<div>
<h1>Advanced Counter</h1>
<div>
<h2>Current Count: {count}</h2>
<h3>Previous Count: {prevCount !== undefined ? prevCount : "N/A"}</h3>
{prevCount !== undefined && (
<p>Difference: {count - prevCount}</p>
)}
</div>
<div>
<label>
Step Value:
<input
type="number"
value={step}
onChange={handleStepChange}
min="1"
/>
</label>
{prevStep !== undefined && prevStep !== step && (
<p>Previous Step: {prevStep}</p>
)}
</div>
<div>
<button onClick={increment}>Increment by {step}</button>
<button onClick={decrement}>Decrement by {step}</button>
<button onClick={reset}>Reset</button>
</div>
</div>
);
}
ALTERNATIVE: USING useReducer WITH PREVIOUS VALUE
Step 6: Counter with useReducer
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
function counterReducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return { count: 0 };
case 'incrementBy':
return { count: state.count + action.payload };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
const prevCount = usePrevious(state.count);
return (
<div>
<h2>Current: {state.count}</h2>
<h3>Previous: {prevCount !== undefined ? prevCount : "N/A"}</h3>
<button onClick={() => dispatch({ type: 'increment' })}>+1</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
<button onClick={() => dispatch({ type: 'incrementBy', payload: 5 })}>+5</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</div>
);
}
KEY CONCEPTS EXPLAINED
1. Why useRef for Previous Value:
- useRef does not trigger re-render when updated
- Value persists across re-renders
- Perfect for storing previous state without causing infinite loops
2. Why useEffect Without Dependencies:
- Runs after every render
- Updates ref.current with the latest value
- Ensures previous value is always one render behind
3. Timing of Updates:
Render 1: count = 0, prevCount = undefined
User clicks increment
Render 2: count = 1, prevCount = 0 (ref updated after render 1)
User clicks increment
Render 3: count = 2, prevCount = 1 (ref updated after render 2)
4. Common Use Cases:
- Tracking value changes
- Showing difference between current and previous
- Conditional rendering based on value change
- Animation triggers
- Undo functionality
COMPARISON: useState vs useRef
Feature | useState | useRef
---------------------------|-----------------------|------------------------
Triggers Re-render | Yes | No
Persists Across Renders | Yes | Yes
Mutable | No (use setter) | Yes (direct mutation)
Use for UI Data | Yes | No
Use for Previous Values | No | Yes
Initialization | useState(value) | useRef(value)
Access Value | state | ref.current
Update Value | setState(newValue) | ref.current = newValue
BEST PRACTICES
1. Always check if previous value is undefined on first render
2. Use usePrevious for tracking state changes
3. Combine with useState for reactive UI updates
4. Use useRef for non-reactive data storage
5. Keep custom hooks reusable and generic
6. Add proper TypeScript types for production code
7. Consider using useReducer for complex state logic
8. Document custom hooks with clear examples