Создание удобной и многократно используемой модальной системы - распространенное требование в современной веб-разработке. Модалы используются для всего, от аутентификации пользователя до уведомлений, и они должны быть доступными, производительными и простыми в управлении в рамках всего приложения. Однако реализация глобальной модальной системы в таком фреймворке, как Next.js, особенно с появлением серверных компонентов, может оказаться непростой задачей. В этой статье я расскажу вам об опыте Selasie Sepenu при создания глобальной модальной системы с использованием Zustand для управления состоянием, React Portals для рендеринга и о трудностях, с которыми он столкнулся на этом пути.
Задача была проста: создать модальное окно, которое можно было бы запускать из любой точки приложения и которое бы последовательно отображалось на всех страницах. Модальная система должна была:
1. Быть глобальной: доступной из любого компонента или страницы.2. Быть производительной: избегать ненужных повторных рендеров.3. Работать с серверными компонентами: в Next.js 13+ используются серверные компоненты, что усложняет ситуацию, поскольку хуки React (например, useStore от Zustand) можно использовать только в клиентских компонентах.
На первый взгляд, это кажется простым. Однако по мере углубления Selasie Sepenu столкнулся с несколькими проблемами, которые потребовали творческих решений.
Первой мыслью было обернуть все приложение в компонент ClientWrapper, который бы управлял логикой модала. Эта обертка использовала бы Zustand для управления состоянием модала и условного отображения модала на основе этого состояния.
Вот как выглядела первоначальная реализация:
"use client";
import { useModalStore } from "@/store/store";
import WaitlistModal from "@/components/ui/Modal";
import { ReactNode } from "react";
interface ClientWrapperProps {
children: ReactNode;
}
export default function ClientWrapper({ children }: ClientWrapperProps) {
const { isModalOpen, closeModal } = useModalStore();
return (
<>
{children}
<WaitlistModal isOpen={isModalOpen} onClose={closeModal} />
</>
);
}
Хотя в теории такой подход работал, он создавал критическую проблему: недоступный код. Когда сначала рендерились дочерние компоненты, WaitlistModal становился недоступным, и наоборот. Это было связано с тем, как React обрабатывает рендеринг компонентов и порядок операций.
Чтобы решить проблему недоступного кода, он обратился к React Portals. Порталы позволяют выводить компонент за пределы родительской иерархии DOM, что идеально подходит для модалов, которые должны отображаться поверх всего остального контента.
Порталы - это функция в React, которая позволяет отображать компонент в узле DOM вне его родительского компонента. Это особенно полезно для модалов, всплывающих подсказок и других оверлейных компонентов, которым нужно вырваться за пределы своего контейнера.
Selasie Sepenu создал компонент Portal для обработки логики рендеринга:
"use client";
import { useEffect, useState } from "react";
import { createPortal } from "react-dom";
interface PortalProps {
children: React.ReactNode;
}
export default function Portal({ children }: PortalProps) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
return () => setMounted(false);
}, []);
if (!mounted) return null;
return createPortal(children, document.body);
}
Далее он обернул компонент WaitlistModal компонентом Portal:
"use client";
import { motion } from "framer-motion";
import Portal from "./Portal";
interface WaitlistModalProps {
isOpen: boolean;
onClose: () => void;
}
const WaitlistModal = ({ isOpen, onClose }: WaitlistModalProps) => {
if (!isOpen) return null;
return (
<Portal>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center z-50"
onClick={onClose}
>
<motion.div
initial={{ y: -50, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: -50, opacity: 0 }}
className="bg-white rounded-lg p-8 w-full max-w-md relative z-60"
onClick={(e) => e.stopPropagation()}
>
{/* Modal content */}
</motion.div>
</motion.div>
</Portal>
);
};
export default WaitlistModal;
Порталы решили проблему недоступного кода, выведя модальное окно за пределы дерева основных компонентов. Благодаря этому модальное окно всегда появлялось над другим контентом, независимо от того, где оно было вызвано.
После того, как модальное окно заработало, как и ожидалось, он столкнулся с новой проблемой: серверные компоненты. В Next.js 13+ используются серверные компоненты, которые отображаются на сервере и не могут использовать такие хуки React, как useState или useStore.
Когда Selasie Sepenu попытался использовать useModalStore в файле app/layout.tsx, он получил следующую ошибку:
Error: useSyncExternalStore only works in Client Components. Add the "use client" directive at the top of the file to use it.
Чтобы решить эту проблему, он создал компонент ModalProvider, который будет обрабатывать модальную логику и отображать WaitlistModal. Этот компонент будет являться клиентским компонентом, что обеспечит возможность использования UseModalStore без проблем.
"use client";
import { useModalStore } from "@/store/store";
import WaitlistModal from "@/components/ui/Modal";
export default function ModalProvider() {
const { isModalOpen, closeModal } = useModalStore();
return <WaitlistModal isOpen={isModalOpen} onClose={closeModal} />;
}
Затем он включил ModalProvider в файл app/layout.tsx:
import { Inter } from "next/font/google";
import "./globals.css";
import ModalProvider from "@/components/ModalProvider";
const inter = Inter({ subsets: ["latin"] });
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className={inter.className}>
{children}
<ModalProvider />
</body>
</html>
);
}
Благодаря этим изменениям глобальная модальная система стала полностью функциональной:
1. Глобальный доступ: модал можно было вызвать из любого компонента с помощью хука useModalStore.2. Эффективность: модальное окно отображалось только тогда, когда это было необходимо, избегая ненужных повторных рендеров.3. Безопасность для сервера: модальная логика обрабатывалась в клиентском компоненте, обеспечивая совместимость с серверными компонентами.
Благодаря этим изменениям глобальная модальная система стала полностью функциональной:
1. Порталы - это мощно: порталы React - отличный инструмент для рендеринга компонентов вне их родительской иерархии, что делает их идеальными для модалов и оверлеев.2. Разделяйте клиентскую и серверную логику: при работе с серверными компонентами Next.js важно разделять логику на стороне клиента в специальных клиентских компонентах.3. Управление состояниями имеет значение: Zustand предоставляет простой и эффективный способ управления глобальным состоянием, но очень важно правильно использовать его в архитектуре серверных компонентов.
Создание глобальной модальной системы в Next.js было сложным, но полезным опытом. Используя React Portals, Zustand и четкое разделение клиентской и серверной логики, Selasie Sepenu смог создать производительное и многократно используемое решение. Если вы работаете над подобным проектом, я надеюсь, эта статья даст вам ценные идеи и поможет избежать распространенных подводных камней.
Счастливого кодинга! 🚀