Everything about React Hooks and Custom Hooks

Hola Product & Tech Geeks 😄!

A few days ago, I uploaded a few blogs regarding JS & ReactJS overview.

I know sometimes it becomes monotonous & sophisticated at the same time to learn these concepts but I tried to make it fun.

So, without any further delay, let’s get started.

Introduction

React Hooks are functions introduced in React 16.8 that allow you to “hook into” React state and lifecycle features from functional components.

They provide a way to use state, side effects (like data fetching), and other React features without needing class components.

Note: Hooks are used for Functional Components only.

But Why Hooks are used?

Ah, yes, Hooks! They’re the sprinkles on the cupcake of functional React components . But why use them instead of good ol’ class components? Let’s dive in with some fun and see what the fuss is about .

#1 Keeping it Simple with a Counter (Class Components were so 2017! )

Imagine a counter component. In a class component, you’d have a constructor, state management, and methods to handle clicks. Here’s an example (brace yourself, it can get verbose ):

class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
handleClick = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.handleClick}>Click me!</button>
</div>
);
}
}

#2 Hooks to the Rescue: Clean, Concise, and Full of Fun (Like a One-Liner Joke! )

With Hooks, things get much cleaner and more readable. We can use the useState hook to manage state and a simple arrow function for the click handler:

function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => setCount(count + 1);
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Click me!</button>
</div>
);
}

See the difference? No more this keyword juggling, just clear variable names and a function that directly updates the state .

#3 Hooks Make Code Reusable: Sharing is Caring (and Saves Time! ⏳)

Let’s say you have a fancy formatting function for displaying the count. With Hooks, you can create a custom hook to encapsulate that logic and reuse it across components:

function useFancyCountFormatter(count) {
const formattedCount = `The count is a whooping ${count}!`; // Your fancy formatting here
return formattedCount;
}
function Counter() {
const [count, setCount] = useState(0);
const formattedCount = useFancyCountFormatter(count); // Reuse the custom hook
const handleClick = () => setCount(count + 1);
return (
<div>
<p>{formattedCount}</p>
<button onClick={handleClick}>Click me!</button>
</div>
);
}

This promotes code organization and reduces redundancy, making your React development a breeze 🪁.

So, there you have it! Hooks bring simplicity, reusability, and a breath of fresh air to functional React components. They might just become your favorite way to build UIs, like that perfect pair of fuzzy socks on a cold day (because comfort and productivity go hand in hand ).

Types of Hooks

Common Hooks:

  1. useState: Creates state variables within functional components.
  2. useEffect: Performs side effects in functional components (data fetching, subscriptions, etc.). Runs after rendering by default.
  3. useContext: Accesses React Context values from functional components.
  4. useReducer: Manages state with a reducer function for complex state logic.
  5. useCallback: Creates memoized callback functions to optimize performance.
  6. useMemo: Memoizes derived values to avoid unnecessary recalculations.

Creating Custom Hooks: You can combine basic hooks to create reusable functionality. This promotes code organization and reduces redundancy.

Let’s Discuss one-by-one:

#2 The Mighty useState Hook: Your One-Stop Shop for State in Functional Components

Imagine a world where functional components couldn’t manage state. It would be like a party without music — sure, it functions, but there’s no real fun .

That’s where useState comes in, the life of the React party, bringing state management to functional components and making them dance the night away .

What is useState?

Think of useState as a magic hat that pulls out state variables for your functional component. You tell it the initial state (like an empty hat), and it gives you back the state value and a function to update it (like pulling a rabbit out and a magic wand to make more appear) ✨.

How to Use It? Let’s Code!

Here’s a simple example: a counter component that keeps track of clicks:

function Counter() {
// Pull the state (count) and the function to update it (setCount) out of the magic hat (useState)
const [count, setCount] = useState(0);
const handleClick = () => {
// Use the magic wand (setCount) to update the count
setCount(count + 1);
};
return (
<div>
<p>You clicked {count} times</p>
<button onClick={handleClick}>Click me!</button>
</div>
);
}

Beyond Numbers: What Can useState Hold?

useState isn't picky! It can hold all sorts of things, not just numbers:

  • Strings: A to-do list item’s text:
const [todoText, setTodoText] = useState('');
  • Objects: User information in a form:
const [user, setUser] = useState({ name: '', email: '' });
  • Arrays: A shopping cart with multiple items:
const [cart, setCart] = useState([]);

Multiple States in One Component? No Problem!

You can have multiple useState calls in a component for independent state variables. Think of it as having multiple hats – more options, more fun!

Remember the Rules!

  • Call useState only at the top level of your component (no loops or conditionals).
  • The state updater function (like setCount) should never be called directly within the JSX (it can be passed as a prop to child components though).

useState Makes Functional Components Powerful

With useState, functional components can manage state effectively, making them strong contenders for building dynamic and engaging UIs. So, grab your magic hat (or rather, your code editor) and start creating some stateful magic with useState! ✨

#2 useEffect: The Versatile Hook for Side Effects in React

Imagine a stage where your React component performs its main act (rendering JSX). But what about the behind-the-scenes magic, the lighting and sound effects that make the show truly spectacular? That’s where useEffect comes in, a versatile hook that lets you handle these "side effects" after the curtain rises .

What are Side Effects?

Side effects are actions that go beyond simply rendering the UI. They might involve:

  • Fetching data from an API
  • Setting up subscriptions to real-time updates
  • Running timers for animations or countdowns ⏱️
  • Manipulating the DOM directly (use with caution! ⚠️)

When to Use useEffect?

Here are some common use cases, each with a fun example to illustrate:

1. Running an Effect After Render (Did Mount)

Think of it as setting up the stage before the play begins. This is the default behavior, where the effect runs after the initial render:

JavaScript

function JokeOfTheDay() {
const [joke, setJoke] = useState(null);
useEffect(() => {
fetch('https://api.example.com/jokes/random')
.then(response => response.json())
.then(data => setJoke(data.joke));
}, []); // Empty dependency array: fetch joke only once on mount

return (
<div>
{joke ? <p>{joke}</p> : <p>Loading a knee-slapper...</p>}
</div>
);
}

2. Running an Effect on Every Render

Imagine a disco ball that keeps spinning throughout the show. This effect runs after every render:

JavaScript

function ColorfulBackground() {
const [color, setColor] = useState('red');
useEffect(() => {
const intervalId = setInterval(() => {
const randomColor = Math.floor(Math.random() * 16777215).toString(16);
setColor(`#${randomColor}`);
}, 1000); // Change color every second
return () => clearInterval(intervalId); // Cleanup: stop interval on unmount
}, []); // Empty dependency array: no change needed to trigger re-run
return (
<div style={{ backgroundColor: color, height: '100vh' }}>
This background is a party!
</div>
);
}

3. Running an Effect When Dependencies Change

Think of it as changing the set design based on the act of the play. The effect runs when the values in the dependency array change:

function UserPosts({ userId }) {
const [posts, setPosts] = useState([]);
useEffect(() => {
fetch(`https://api.example.com/users/${userId}/posts`)
.then(response => response.json())
.then(data => setPosts(data));
}, [userId]); // Re-fetch posts only when userId changes
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}

4. Cleaning Up After Effects (Did Unmount)

Imagine taking down the set after the final curtain call. This cleanup function ensures proper resource management:

function Subscription() {
const [subscription, setSubscription] = useState(null);
useEffect(() => {
const newSubscription = /* create your subscription logic here */
setSubscription(newSubscription);
return () => {
newSubscription.unsubscribe(); // Cleanup: unsubscribe on unmount
};
}, []); // Empty dependency array: create subscription only once
return (
<div>You are subscribed!</div>
);
}

useEffect: A Powerful Tool for Side Effects

By understanding these use cases, you can leverage useEffect to manage various side effects in your React components. Remember, use it wisely to keep your components performant and avoid unnecessary re-renders!

#3 useContext: Sharing is Caring (and Makes Code Cleaner!) in React

Imagine a massive mansion (your React application) with many rooms (components). Each room needs access to certain information, like the current theme (dark or light) or the logged-in user’s data. Passing this information down through every component can be a tedious chore, like carrying a heavy box through the entire house . Here’s where useContext comes in, a handy hook that lets you share data across components without the hassle .

What is useContext?

Think of useContext as a magical intercom system within your React mansion. You define a "context" that holds the shared data (like the theme or user info), and components can then "listen in" to access it, eliminating the need for prop drilling (passing data down through every component) ️.

Setting Up the Context

#3.1 Create the Context:
Use React.createContext to define the context and its initial value. Think of it as setting up the intercom system and assigning a default channel.

const ThemeContext = React.createContext('light'); // Default theme

#3.2 Provide the Context:
Wrap your component tree with a Context.Provider component. This makes the context value available to all its descendants. Imagine placing the intercom system in a central location within the mansion.

function App() {
const [theme, setTheme] = useState('light');
const toggleTheme = () => setTheme(theme === 'light' ? 'dark' : 'light');
return (
<ThemeContext.Provider value={theme}>
{/* All child components here */}
</ThemeContext.Provider>
);
}

#3.3 Accessing the Context in Components

Use the useContext hook within any component that needs to access the shared data. Think of it as a child component picking up the intercom receiver and listening in.

function Header() {
const theme = useContext(ThemeContext); // Access theme from context
return (
<header style={{ backgroundColor: theme === 'light' ? 'white' : 'black' }}>
Header
</header>
);
}

Use Cases for useContext

Here are some scenarios where useContext shines:

  • Global Theme: Share the current theme (light or dark) across your application for a consistent look and feel .
  • User Authentication: Provide user information (like name, email) to components that need it, without prop drilling .
  • Localization: Share the current language setting across the app to display localized content .

Remember the Rules!

  • Use useContext sparingly for truly global data. Overuse can create tight coupling between components.
  • Consider alternatives like Redux or Zustand for complex state management needs.

useContext: A Communication Booster for React Apps

By using useContext effectively, you can create a more organized and maintainable React application where components can easily share data without getting lost in a maze of props.

So, ditch the heavy boxes (prop drilling) and pick up the intercom (useContext) for smoother communication in your React mansion! ️

#4 useReducer: The State Management Powerhouse for Functional Components

Imagine a complex React component, juggling multiple state values and intricate logic updates. It’s like a chef in a bustling kitchen, keeping track of ingredients, timers, and orders ‍. That’s where useReducer comes in, a powerful hook that acts like a head chef, handling complex state management with a clear and organized approach. Let's delve into its world with some fun examples! ✨

What is useReducer?

Think of useReducer as a delegation master. Instead of directly updating state within a component, you create a reducer function . This function takes the current state and an action object, then returns the new state based on the action type. This separation keeps your component logic clean and makes complex state updates a breeze .

Why Use useReducer?

Here’s when useReducer shines:

  • Multiple State Values: Managing multiple state values becomes easier with a centralized reducer function .
  • Complex Logic: When updating state involves complex calculations or interactions between values, useReducer provides a structured approach .
  • Shared State Logic: If you need to share state update logic across multiple components, a reducer can be a reusable solution .

Let’s Get Cooking! A Shopping Cart Example

Here’s a shopping cart component that uses useReducer to manage items, quantities, and the total price:

1. Define the Initial State:

const initialState = {
items: [],
total: 0,
};

2. Create the Reducer Function (The Head Chef):

const reducer = (state, action) => {
switch (action.type) {
case 'ADD_ITEM':
return {
...state,
items: [...state.items, action.payload],
total: state.total + action.payload.price,
};
case 'REMOVE_ITEM':
const newItems = state.items.filter(item => item.id !== action.payload.id);
const newTotal = state.total - action.payload.price;
return { ...state, items: newItems, total: newTotal };
default:
return state;
}
};

3. Implement the Component with useReducer:

function ShoppingCart() {
const [cartState, dispatch] = useReducer(reducer, initialState); // Get state and dispatch function
const addToCart = (item) => dispatch({ type: 'ADD_ITEM', payload: item }); // Dispatch an action to add
const removeItem = (item) => dispatch({ type: 'REMOVE_ITEM', payload: item }); // Dispatch an action to remove
// ... rest of your component logic to display cart items, total, and buttons
}

Benefits of useReducer:

  • Clear State Management: Reduces boilerplate code and improves readability.
  • Complex Logic Handling: Encapsulates complex state updates in a single function.
  • Shared State Logic: Reusable reducer functions can be shared across components.

Remember:

  • useReducer adds some overhead compared to useState. Use it for complex state scenarios.
  • Consider a state management library like Redux for large-scale applications with many complex states.

useReducer: A Powerful Tool for Complex State Management

By mastering useReducer, you can handle complex state updates in your React components with ease. It's like having a head chef in charge, ensuring your application's state is always well-organized and under control! ‍

#5 useCallback: The Performance Booster for Callback Functions in React

Imagine a React component with a fancy button that triggers a complex calculation . Every time the component re-renders (even for minor UI changes), the calculation function gets recreated ‍. This can lead to performance issues, especially with expensive calculations. That’s where useCallback comes in, a superhero hook that prevents unnecessary function recreations and keeps your components running smoothly !

What is useCallback?

Think of useCallback as a memory booster for your React components. It takes a function and an array of dependencies as arguments. It then "remembers" the function and only returns a new version if one of the dependencies changes . This way, child components that rely on this function as a prop only receive an updated version if the function itself has actually changed based on its dependencies.

When to Use useCallback?

Here are some situations where useCallback can be your performance hero:

  • Expensive Callback Functions: If a callback function involves complex calculations or interacts with external libraries, memoizing it with useCallback can significantly improve performance.
  • Preventing Unnecessary Re-renders in Child Components: When a child component relies on a prop that is a callback function, useCallback ensures the child only re-renders if the function itself has changed due to its dependencies.

Let’s Code! A Memoized Filter Example

Here’s an example of a parent component with a filter function and a child component that displays filtered data:

1. Parent Component with useCallback:

function ProductsList() {
const [products, setProducts] = useState([]);
const [searchTerm, setSearchTerm] = useState('');
const filterProducts = useCallback(() => {
return products.filter(product => product.name.includes(searchTerm));
}, [products, searchTerm]); // Memoize based on products and searchTerm
// ... fetch products logic
return (
<div>
<input value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} />
<ProductList items={filterProducts()} />
</div>
);
}

2. Child Component Receiving Filtered Data:

function ProductList({ items }) {
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}

Benefits of useCallback:

  • Improved Performance: Prevents unnecessary re-renders of child components due to memoized callback functions.
  • Cleaner Code: Separates the logic of creating the callback function from the component rendering.

Remember:

  • Only use useCallback when necessary, as it adds a slight overhead.
  • The dependency array is crucial to ensure the function is recreated only when needed.

useCallback: Your Performance Partner in React

By using useCallback strategically, you can optimize your React components and ensure they run smoothly even with complex calculations or interactions. So, put on your performance cape and embrace the power of memoization!

#6 useMemo: The Speedy Memoization Hook for Expensive Calculations in React

Imagine a React component that relies on a complex calculation to display some data . Every time the component re-renders (even for minor UI changes), the calculation gets re-run . This can be a performance drain, especially if the calculation is time-consuming. Enter useMemo, a superhero hook that comes to the rescue by memoizing the result of the calculation, preventing unnecessary re-computations and keeping your components zippy !

What is useMemo?

Think of useMemo as a super-powered notepad for your React components. It takes a function (the calculation) and an array of dependencies as arguments. It then calculates the result of the function only once and stores it in memory. If the component re-renders and the dependencies haven't changed, useMemo simply returns the stored result, saving precious processing power .

When to Use useMemo?

Here are some situations where useMemo can be your performance champion:

  • Expensive Calculations: If a calculation involves complex logic, data manipulation, or external library calls, memoizing it with useMemo can significantly improve performance.
  • Derived Data in Child Components: When a child component relies on data derived from props received from the parent, useMemo can prevent unnecessary re-renders in the child if the props haven't changed but the derived data requires a calculation.

Let’s Code! A Memoized Distance Calculation Example

Here’s an example of a component that calculates the distance between two points based on user input:

Parent Component with useMemo:

function MapView({ userLocation, selectedLocation }) {
const distance = useMemo(() => {
// Complex distance calculation logic using userLocation and selectedLocation
return calculateDistance(userLocation, selectedLocation);
}, [userLocation, selectedLocation]); // Memoize based on userLocation and selectedLocation
return (
<div>
<p>Distance: {distance} km</p>
{/* ... other map & location UI */}
</div>
);
}

Benefits of useMemo:

  • Improved Performance: Prevents unnecessary re-calculations of expensive functions.
  • Cleaner Code: Separates the logic of the calculation from the component rendering.

Remember:

  • Only use useMemo when necessary, as it adds a slight overhead of creating the function and potentially storing the result.
  • The dependency array is crucial to ensure the calculation is re-run only when the relevant data changes.

useMemo: Your Speedy Calculation Partner in React

By strategically using useMemo, you can optimize your React components for performance and ensure they display data efficiently even with complex calculations. So, grab your memo pad (or rather, your code editor) and embrace the power of memoization!

Custom Hooks: Building Your Own React Toolkit ✨

Imagine a toolbox full of specialized tools for every React development task. That’s the power of custom hooks! They’re like pre-built functions that encapsulate common logic or functionality, making your components more modular, reusable, and easier to maintain . Let’s dive into the workshop and craft some custom hooks that will make your React development a breeze!

What are Custom Hooks?

Think of custom hooks as superheroes in your React component toolbox. They are regular JavaScript functions that start with use (to signal they use React hooks) and can use other hooks inside them. The magic happens when you return the desired values (like state or functions) from the custom hook. This allows you to share that functionality across multiple components without code duplication .

Why Use Custom Hooks?

Here are some compelling reasons to embrace custom hooks:

  • Code Reusability: Don’t repeat yourself! Encapsulate common logic in a custom hook and use it across components.
  • Improved Readability: Break down complex component logic into smaller, more focused custom hooks.
  • Better Maintainability: Changes to a custom hook propagate easily to all components that use it.

Let’s Code! Building a Fetch Data Hook

Here’s a custom hook that fetches data from an API:

function useFetchData(url) {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
try {
const response = await fetch(url);
const data = await response.json();
setData(data);
} catch (error) {
setError(error);
} finally {
setIsLoading(false);
}
};
fetchData();
}, [url]); // Re-fetch only if url changes
return { data, isLoading, error };
}

How to Use the Custom Hook?

Here’s how a component can utilize the useFetchData hook to display fetched data:

function MyComponent() {
const { data, isLoading, error } = useFetchData('https://api.example.com/data');
if (isLoading) {
return <p>Loading data...</p>;
}
if (error) {
return <p>Error: {error.message}</p>;
}
return (
<div>
<h1>{data.title}</h1>
<p>{data.content}</p>
</div>
);
}

Benefits of Custom Hooks:

  • Organized Code: Custom hooks promote a clean separation of concerns.
  • Testability: Isolated logic in custom hooks makes them easier to test.
  • Community Sharing: Share reusable custom hooks across projects or with the React community.

Remember:

  • Custom hooks are not a silver bullet. Use them strategically for reusable and complex logic.
  • Name your custom hooks clearly and descriptively.

Custom Hooks: Empowering Your React Development

By mastering custom hooks, you can elevate your React development skills and build more efficient, maintainable, and scalable applications. So, unleash your creativity and start building your own custom hook toolbox!

--

--