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


🚀 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 viauseState
.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 inlocalStorage
(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!
3. NavLink in React Router:
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.
What is NavLink?
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.
When to Use NavLink?
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
orstyle
functions. - Can be used to build active tab navigation, breadcrumbs, etc.
Basic Syntax:
import { NavLink } from "react-router-dom";
<NavLink to="/about">About</NavLink>
className Callback in 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.
Example: Tailwind Styling for Active Link
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
istrue
). - If it is, it returns Tailwind classes:
text-blue-700 underline
. - Otherwise, it returns an empty string.
- The
className
is 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 theAbout
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()
andSuspense
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!
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.