Oct 11, 2024
•
6 min read
So I was working on this feature at Veryable where we needed to open a confirmation modal from like 6 different places, and every time I needed to add another trigger point, I died a little inside. Prop drilling through 4-5 components, keeping track of which modal was open, what data it needed... it was getting ridiculous.
Then someone on the team mentioned nuqs and honestly, it sounded weird at first. "Put your modal state in the URL?" But after trying it, I'm never going back.
Picture this: You've got a user edit modal. You need to open it from:
So you end up with this mess:
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [editingUserId, setEditingUserId] = useState(null);
// Pass these down through 47 components
// Hope nobody refreshes the page
// Die inside when product asks to add another trigger point
Yeah. Not great.
First, install nuqs:
npm install nuqs
The idea is simple - your modal state lives in the URL. So instead of useState, you use nuqs to manage query parameters. Here's what my actual hook looks like:
// hooks/useUserEditModal.ts
import { parseAsBoolean, parseAsInteger, useQueryStates } from 'nuqs';
export const useUserEditModal = () => {
return useQueryStates({
editUserModal: parseAsBoolean.withDefault(false),
userId: parseAsInteger.withDefault(0)
});
};
That's literally it. Your URL becomes ?editUserModal=true&userId=123 and now you can read/write that state from anywhere.
The modal itself:
// components/UserEditModal.tsx
'use client';
import { useUserEditModal } from '@/hooks/useUserEditModal';
export const UserEditModal = () => {
const [{ editUserModal, userId }, setModalState] = useUserEditModal();
const handleClose = () => {
setModalState({ editUserModal: false, userId: 0 });
};
if (!editUserModal) return null;
return (
<Modal open={editUserModal} onClose={handleClose}>
<div>Editing user {userId}</div>
<button onClick={handleClose}>Close</button>
</Modal>
);
};
And then literally anywhere else in your app:
// Could be in a totally different file, doesn't matter
import { useUserEditModal } from '@/hooks/useUserEditModal';
export const SomeRandomComponent = () => {
const [_, setModalState] = useUserEditModal();
return (
<button onClick={() => setModalState({ editUserModal: true, userId: 123 })}>
Edit User
</button>
);
};
No prop drilling. No context. Just works.
Deep linking just works: Someone sends you yourapp.com/dashboard?editUserModal=true&userId=42 and the modal pops right open. I didn't have to write any special code for this. Product manager sent me a URL with a bug in it, modal opened to the exact state, I fixed it. Felt like magic.
Browser back button: Users can close modals by hitting back. Again, didn't write any code for this. It just works because the URL changes.
Multiple modals: Just add more properties to your hook:
export const useAllMyModals = () => {
return useQueryStates({
editUser: parseAsBoolean.withDefault(false),
deleteUser: parseAsBoolean.withDefault(false),
userId: parseAsInteger.withDefault(0),
settingsOpen: parseAsBoolean.withDefault(false),
// whatever else you need
});
};
Don't put everything in one hook: I made this mistake. Had one giant useAllModals() hook and it got messy fast. Split them up by feature area.
Add helper functions to make life easier:
export const useUserEditModal = () => {
const [state, setState] = useQueryStates({
editUserModal: parseAsBoolean.withDefault(false),
userId: parseAsInteger.withDefault(0)
});
const openModal = (userId: number) => {
setState({ editUserModal: true, userId });
};
const closeModal = () => {
setState({ editUserModal: false, userId: 0 });
};
return { isOpen: state.editUserModal, userId: state.userId, openModal, closeModal };
};
Now it's just:
const { openModal } = useUserEditModal();
<button onClick={() => openModal(123)}>Edit</button>
Server components don't work with this: Learned this the hard way. You need 'use client' at the top of any file using nuqs. It's a client-side thing because... well, URLs change on the client.
Don't put sensitive stuff in URLs: I hope this is obvious but I'm saying it anyway. User IDs? Fine. Passwords or tokens? No.
I was skeptical at first. "Managing state in the URL sounds hacky" is what I thought. But after using it for a few weeks, I can't imagine going back to the old way.
No more prop drilling through 5 components just to open a modal. No more "wait, where did I define that state again?" No more losing modal state on refresh and having users complain.
The URL is basically a global state store that's been there the whole time. We just forgot about it because we were too busy installing state management libraries.
Anyway, try it out. Worst case, you spend 30 minutes and decide it's not for you. Best case, you never prop drill modal state again.
Worth it.
© 2025 Lucas Nogueira. All rights reserved.
Built with Next.js & Material-UI