DAY 22: Mastering React Router NavLink, React Portals, Code Splitting, and Lazy Loading | My Web Dev Journey – ReactJS

Ritik KumarRitik Kumar
11 min read

🚀 Introduction

Welcome to Day 22 of my Web Development Journey!
Over the past few weeks, I’ve been diving deep into ReactJS after wrapping up the fundamentals of HTML, CSS, and JavaScript.

Recently, I’ve explored key concepts such as:
NavLink in React Router, React Portal, Code Splitting and Lazy Loading, created a Modal using React Portal, and built Custom useFilter and useLocalStorage Hooks.

I’m documenting this journey publicly — both to stay consistent and to help others learning React from scratch.

📅 Here’s What I Learned Over the Last 3 Days:

Day 19:

  • Custom useFilter Hook
  • Custom useLocalStorage Hook

Day 20:

  • NavLink in React Router
  • React Portal
  • Created Modal/Popup using React Portal

Day 21:

  • Code Splitting and Lazy Loading

Let’s break down each of these topics below 👇


1. Custom useFilter Hook:

One of the coolest things I’ve learned in React recently is how to write custom hooks. Instead of repeating logic across components, custom hooks allow us to extract and reuse stateful behavior cleanly.

Let’s dive into how I built it, why it works, and how you can use it in your own projects.

Why I Built a useFilter Hook?

In many applications, we need to filter a list of data — like:

  • Searching a list of expenses by category
  • Filtering bookmarks by tag
  • Narrowing down products by name or type

Instead of repeating this logic every time, I decided to build a reusable and flexible hook that can:

  • Accept any dataset
  • Let us choose how we want to filter the data
  • Expose the filtered list and the search query handler

How It Works?

Here’s the complete code:

import { useState } from "react";

export function useFilter(dataList, callback) {
  const [query, setQuery] = useState("");

  const filteredData = dataList.filter((data) => {
    const lowerQuery = query.toLowerCase();
    return callback(data).toLowerCase().includes(lowerQuery);
  });

  return [filteredData, setQuery];
}

Let’s break this down:

  • dataList: This is the full array of data we want to filter (e.g., expenses).
  • callback: A function that tells the hook what property or value to filter by (e.g., data.category).
  • query: The search input or text, stored in state via useState.
  • setQuery: A function we can call when the user types something new.
  • filteredData: The final list after applying the filter logic — only the items that match the query.

How I Use It in My App:

Here’s how I implemented the hook in my Expense Tracker project:

const [filteredData, setQuery] = useFilter(expenses, (data) => data.category);

This means:

  • I want to filter the expenses array
  • I’m interested in filtering based on data.category
  • As the query changes, I’ll get a new filtered list in filteredData
  • I can pass setQuery to an input field to update the filter

Example usage with an input:

<input
  type="text"
  placeholder="Search by category"
  onChange={(e) => setQuery(e.target.value)}
/>

{filteredData.map((expense) => (
  <div key={expense.id}>{expense.category} - ${expense.amount}</div>
))}

Why This Hook Is Powerful?

This hook makes filtering dynamic and reusable:

  • Works with any data: expenses, users, products, anything
  • Easy to plug in and use anywhere
  • Keeps filtering logic clean and centralized
  • Uses functional programming (passing functions like callback) to stay flexible

Final Thoughts:

Building the useFilter hook helped me understand how to:

  • Abstract reusable logic into custom hooks
  • Write flexible, declarative code using callbacks
  • Enhance component reusability and readability

2. Custom useLocalStorage Hook:

I built a custom useLocalStorage hook to persist state even after the page reloads — perfect for things like saving form input or application data (like expenses).

What This Hook Does?

React’s useState doesn’t persist data between page reloads, but localStorage does.
So I created a custom hook that combines both: React state + browser localStorage.

Core Logic:

Here’s the core logic I implemented:

import { useState } from "react";
import { initLocalStorage } from "../utility/initLocalStorage";

export function useLocalStorage(key, initialData) {
  const [data, setData] = useState(initLocalStorage(key, initialData));

  const updateLocalStorage = (newData) => {
    const valueToStore =
      typeof newData === "function" ? newData(data) : newData;
    localStorage.setItem(key, JSON.stringify(valueToStore));
    setData(valueToStore);
  };

  return [data, updateLocalStorage];
}

It also uses a small utility function to initialize the data:

export const initLocalStorage = (key, initialData) => {
  const saved = localStorage.getItem(key);
  if (saved !== null) {
    return JSON.parse(saved);
  }
  localStorage.setItem(key, JSON.stringify(initialData));
  return initialData;
};

Let’s Break It Down:

  • key: The name used to store the data in localStorage (e.g., "expenses").
  • initialData: The default value if nothing is found in localStorage.
  • initLocalStorage: Checks if data already exists in localStorage. If it does, it uses it. Otherwise, it saves the default value.
  • data: Our stateful data, just like useState.
  • updateLocalStorage: A function that updates both state and localStorage.

How I Use It?

Here’s how I used this custom hook in my app:

const [expenses, setExpenses] = useLocalStorage("expenses", expenseData);

const [inputData, setInputData] = useLocalStorage("inputData", {
  title: "",
  category: "",
  amount: "",
});

This allows me to:

  • Keep expenses stored across page reloads.
  • Keep form input values persistent if the user refreshes the page or comes back later.

Final Thoughts:

This hook made my app more user-friendly and persistent without having to write repetitive localStorage logic everywhere.

It also helped me:

  • Understand how to abstract localStorage logic
  • Combine useState and browser APIs together
  • Keep my components clean and focused on UI, while logic lives in the hook

Now I can reuse this hook in any app that needs local persistence!


When building navigation in React applications using React Router, we often need a way to highlight the active link — this is where NavLink comes in.

NavLink is a special version of the standard Link component provided by react-router-dom.
It allows us to apply active styles or class names automatically when the link matches the current route.

Use NavLink instead of Link when:

  • We want to highlight the currently active route in the navbar.
  • We need dynamic styling based on whether the link is active.
  • We're building navigational UI (sidebars, top navs, tabs, etc.).

Key Features:

  • Adds an active class by default to the link when the route is matched.
  • Supports dynamic styling using className or style functions.
  • Can be used to build active tab navigation, breadcrumbs, etc.

Basic Syntax:

import { NavLink } from "react-router-dom";

<NavLink to="/about">About</NavLink>

When using NavLink in React Router, we can pass a callback function to the className prop.
This function receives an object with useful properties, and we can return a string based on its values.

What Gets Passed to the Callback?

{ isActive, isPending, isTransitioning }
  • isActive: true if the link matches the current URL.
  • isPending: true if the navigation to that route is still loading (React Router v6.4+).
  • isTransitioning: true if the route transition is ongoing.

We mostly use isActive for styling purposes.

Here’s a simple NavLink setup using Tailwind classes:

const activeLink = ({ isActive }) =>
  isActive ? "text-blue-700 underline" : "";

<li>
  <NavLink className={activeLink} to="/about">
    About
  </NavLink>
</li>

How It Works:

  • The activeLink function checks if the link is active (isActive is true).
  • If it is, it returns Tailwind classes: text-blue-700 underline.
  • Otherwise, it returns an empty string.
  • The classNameis applied conditionally based on the route match.

So when we're on the /about route, the "About" link will appear with blue text and an underline.

Final Thoughts:

  • NavLink enhances the UX by visually indicating the active page.
  • It's a drop-in replacement for Link when you need styling based on the current route.
  • Easy to integrate with CSS or utility libraries like Tailwind.
  • Use NavLink in every React app that has a navigation bar — it's a small change that makes a big difference!

4. React Portal:

When building modern web apps, we often need to show modals, popups, tooltips, or dropdowns that float above the main UI. But rendering such components inside deeply nested React trees can lead to CSS or z-index conflicts.

That’s where React Portals shine.

What is a React Portal?

A React Portal lets us render a component outside the main React DOM hierarchy while still preserving the React state and event handling.

Normally, everything renders inside the root <div id="root">, but with portals, we can render into another DOM node (like <div id="portal">).

Why Use React Portals for Modals?

  • Modals need to overlay the entire app regardless of where they're triggered from.
  • Avoid z-index issues caused by nested components.
  • Keep modal logic declarative and reusable.
  • Separate modal DOM from app structure while keeping React context.

How to Use React Portals?

Step 1: Add a modal root in index.html

<body>
  <div id="root"></div>
  <div id="portal"></div> <!-- Portal root added -->
  <script type="module" src="/src/main.jsx"></script>
</body>

Step 2: Create a Modal component using createPortal

import { createPortal } from "react-dom";

export default function Modal({ isOpen, setIsOpen, header, footer, children }) {
  return createPortal(
    <div
      onClick={() => setIsOpen(false)}
      className={`${
        !isOpen ? "hidden" : ""
      } fixed inset-0 flex items-center justify-center bg-black/40 px-4`}
    >
      <div
        onClick={(e) => e.stopPropagation()}
        className="max-w-2xl grow rounded-lg bg-white p-4 shadow-lg"
      >
        {header}
        <div className="-mx-4 my-3 flex flex-wrap gap-4 border-y px-4 py-4">
          {children}
        </div>
        {footer}
      </div>
    </div>,
    document.getElementById("portal") //  rendered outside root
  );
}

Example: How I Used It?

<li>
  <button onClick={() => setIsOpen(true)} className="cursor-pointer">
    Sign In
  </button>
  <Modal
    isOpen={isOpen}
    setIsOpen={setIsOpen}
    header={<div className="text-xl font-bold">Sign In</div>}
    footer={
      <div className="flex justify-end gap-4">
        <button
          onClick={() => setIsOpen(false)}
          className="cursor-pointer rounded-md bg-gray-300 px-6 py-2 font-semibold hover:bg-gray-400/80"
        >
          Cancel
        </button>
        <button
          onClick={() => setIsOpen(false)}
          className="cursor-pointer rounded-md bg-blue-300 px-6 py-2 font-semibold hover:bg-blue-400/80"
        >
          Sign In
        </button>
      </div>
    }
  >
    <input
      placeholder="Username"
      className="grow rounded border border-gray-600 px-2 py-1"
      type="text"
    />
    <input
      placeholder="Password"
      className="grow rounded border border-gray-600 px-2 py-1"
      type="password"
    />
  </Modal>
</li>

Advantages of Using React Portals:

  • Decouples layout and modals — easier to manage styling
  • Works seamlessly with event bubbling
  • Makes modals reusable across the app
  • Fixes z-index/styling problems in deeply nested components
  • Keeps our main component tree cleaner

Final Thoughts:

  • React Portals are a powerful feature that make building modals, tooltips, and dropdowns much cleaner and more manageable.
  • By rendering outside the main component hierarchy, we gain better control over styling, positioning, and event handling — without breaking our app structure.
  • If our app includes any overlay UI elements like modals or popups, portals are the recommended way to implement them. Once we understand how they work, we'll find ourself using them in every serious React project!

5. Code Splitting and Lazy Loading:

As our React applications grow in size and complexity, loading everything at once can negatively impact performance. This is where code splitting and lazy loading come in to help optimize the user experience.

What is Code Splitting?

Code splitting is the process of breaking a large JavaScript bundle into smaller chunks that can be loaded on demand. Instead of shipping the entire app to the browser in one go, React can load only what’s needed when it’s needed.

React apps built with tools like Vite or Webpack support code splitting out of the box when we use dynamic import() statements.

What is Lazy Loading?

Lazy loading is a technique where we delay the loading of a component until it's actually needed — for example, when the user navigates to a specific route.

React provides the React.lazy() API to enable lazy loading of components, and Suspense to show a fallback UI while the component is loading.

How to Use Lazy Loading with React.lazy() and Suspense:

Here’s a simple example of how to lazy-load a component:

import React, { lazy, Suspense } from "react";

// Lazy-loaded component
const About = lazy(() => import("./pages/About"));

export default function App() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <About />
      </Suspense>
    </div>
  );
}

Here,

  • React.lazy() dynamically imports the About component only when it's needed, reducing the initial load time.
  • Suspense wraps the lazy-loaded component and shows a fallback UI (like a spinner or a "Loading..." message) until the component finishes loading.

Benefits of Code Splitting & Lazy Loading:

  • Improved Performance – Faster initial page load by avoiding unnecessary code.
  • Reduced Bundle Size – Only loads what’s needed, when it’s needed.
  • Better User Experience – Avoids long loading times for pages/components that users may never visit.
  • Cleaner Architecture – Encourages modular, route-based, or component-based chunking of code.

Final Thoughts:

  • React.lazy() and Suspense bring dynamic loading to our React app with minimal effort.
  • It’s a must-have technique for large apps with many routes or components.
  • Keeps our initial bundle lean and responsive, which improves Core Web Vitals.
  • Pair with route-based code splitting for maximum impact — especially in real-world production apps.

6. What’s Next:

I’m excited to keep growing and sharing along the way! Here’s what’s coming up:

  • Posting new blog updates every 3 days to share what I’m learning and building.
  • Diving deeper into Data Structures & Algorithms with Java — check out my ongoing DSA Journey Blog for detailed walkthroughs and solutions.
  • Sharing regular progress and insights on X (Twitter) — feel free to follow me there and join the conversation!

Thanks for being part of this journey!


7. Conclusion:

In this part of my web development journey, I explored some essential React tools and patterns that helped me write cleaner, more scalable, and user-friendly applications:

  • Built a custom useFilter hook to make filtering flexible and reusable across components.
  • Created a useLocalStorage hook to persist data easily, while keeping components clean and focused.
  • Learned how NavLink simplifies dynamic navigation styling and improves overall user experience.
  • Discovered the power of React Portals for clean, accessible modal implementations without layout issues.
  • Leveraged Code Splitting and Lazy Loading to boost performance by reducing bundle size and speeding up load time.

Each of these concepts made me a more confident React developer and improved the quality of my codebase. These patterns aren't just tools — they’re practices that shape how we architect modern, performant React applications.

If you're on a similar journey, feel free to follow along or connect — we’re all in this together!

0
Subscribe to my newsletter

Read articles from Ritik Kumar directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ritik Kumar
Ritik Kumar

👨‍💻 Aspiring Software Developer | MERN Stack Developer.🚀 Documenting my journey in Full-Stack Development & DSA with Java.📘 Focused on writing clean code, building real-world projects, and continuous learning.