Published on

React Portal in Next.js and the Sticky Footer

Last Modified on
Last modified on
Authors
Portal in Belgrade Fortress, Serbia
Photo by Ivan Aleksic on Unsplash

Anyone who has been following my posts knows that I have a thing about sticky footers, and that I have applied various techniques over the years in various situations. Well, I came across a new situation working with Next.js.

For my revamp of my personal site mariadcampbell.com, I created a React Portal for my notification component which sends a message regarding the status of their contact form submission. There, I place the notification component below the contact form element inside the contact-form.js file of the ContactForm component. The notification markup consists of the following:

return (
	<div className={cssClasses}>
		<h2>{title}</h2>
		<p>{message}</p>
	</div>
)

And the returned jsx of the ContactForm component is the following:

<section className={`contact ${classes.contact}`}>
	<h1>How can I help you?</h1>
	<form className={classes.form} onSubmit={sendMessageHandler}>
		<div className={classes.controls}>
			<div className={classes.control}>
				<label className={`label`} htmlFor="name">
					Your Name
				</label>
				<input
					type="text"
					id="name"
					required
					value={enteredName}
					onChange={(event) => setEnteredName(event.target.value)}
				/>
			</div>
			<div className={classes.control}>
				<label className={`label`} htmlFor="twitter-handle">
					Your TwitterHandle
				</label>
				<input
					type="text"
					id="twitter-handle"
					required
					value={enteredTwitterHandle}
					onChange={(event) =>
						setEnteredTwitterHandle(event.target.value)
					}
				/>
			</div>
			<div className={classes.control}>
				<label className={`label`} htmlFor="linkedin-handle">
					Your Linkedin Handle
				</label>
				<input
					type="text"
					id="linkedin-handle"
					required
					value={enteredLinkedinHandle}
					onChange={(event) =>
						setEnteredLinkedinHandle(event.target.value)
					}
				/>
			</div>
			<div className={classes.control}>
				<label className={`label`} htmlFor="github-handle">
					Your Github Handle
				</label>
				<input
					type="text"
					id="github-handle"
					required
					value={enteredGithubHandle}
					onChange={(event) =>
						setEnteredGithubHandle(event.target.value)
					}
				/>
			</div>
		</div>
		<div className={classes.control}>
			<label className={`label`} htmlFor="message">
				Your Message
			</label>
			<textarea
				type="text"
				id="message"
				rows="5"
				required
				value={enteredMessage}
				onChange={(event) => setEnteredMessage(event.target.value)}
			></textarea>
		</div>
		<div className={classes.actions}>
			<button className={`contact-btn-submit`} type="submit">
				Send Message
			</button>
		</div>
	</form>
	{notification && (
		<DynamicNotification
			status={notification.status}
			title={notification.title}
			message={notification.message}
		/>
	)}
</section>

This markup is not quite right as far as HTML structure goes. I ended up getting hydration errors as a result. So to avoid this, I created a React Portal for my Notification component. Like the following:

import { createPortal } from 'react-dom'

import classes from '../../styles/notifications.module.scss'

function Notification(props) {
	const { title, message, status } = props

	let statusClasses = ''

	if (status === 'success') {
		statusClasses = classes.success
	}

	if (status === 'error') {
		statusClasses = classes.error
	}

	const cssClasses = `${classes.notification} ${statusClasses}`

	return createPortal(
		<div className={cssClasses}>
			<h2>{title}</h2>
			<p>{message}</p>
		</div>,
		document.getElementById('notifications'),
	)
}

export default Notification

But THEN, when I wanted to add my sticky footer as I did in the past, I ended up getting a hydration error in development. Please visit my post entitled The sticky footer and Next.js 13 to learn more. So I created a React Portal for my Footer component. I did the following:

function Footer() {
	const { data: session, status } = useSession()

	if (typeof document === 'object') {
		return createPortal(
			<footer className={`footer ${classes.footer}`}>
				{status === `authenticated` && <DynamicFooterNavigation />}
				{status === `unauthenticated` && (
					<p>
						You need to sign in to access the Contact and Guestbook
						pages.
					</p>
				)}
				{status === `authenticated` && (
					<div
						className={`provider-button ${classes['provider-button']}`}
					>
						<button onClick={() => signOut()}>Sign out</button>
					</div>
				)}
				{status === `unauthenticated` && (
					<div
						className={`provider-button ${classes['provider-button']}`}
					>
						<button onClick={() => signIn()}>
							Sign in with Github
						</button>
					</div>
				)}
				<h2 className={`${classes.follow} ${oswald.variable}`}>
					Follow
				</h2>
				<div className={`${classes['svg-wrapper']}`}>
					<div className={`footer-email ${classes['footer-email']}`}>
						<DynamicSocialIcon
							name="email"
							href={`mailto:${siteMetadata.email}`}
							size="6"
						/>
					</div>
					<div
						className={`footer-github ${classes['footer-github']}`}
					>
						<DynamicSocialIcon
							name="github"
							href={siteMetadata.github}
							size="6"
						/>
					</div>
					<div
						className={`footer-twitter ${classes['footer-twitter']}`}
					>
						<DynamicSocialIcon
							name="twitter"
							href={siteMetadata.twitter}
							size="6"
						/>
					</div>
					<div
						className={`footer-linkedin ${classes['footer-linkedin']}`}
					>
						<DynamicSocialIcon
							name="linkedin"
							href={siteMetadata.linkedin}
							size="6"
						/>
					</div>
					<div
						className={`footer-sitemap ${classes['footer-sitemap']}`}
					>
						<DynamicSocialIcon
							name="sitemap"
							href={siteMetadata.sitemap}
							size="6"
						/>
					</div>
				</div>
				<p>
					{`© ${new Date().getFullYear()}`} {``}{' '}
					{siteMetadata.author} {``}
					<Link href="/">{siteMetadata.title}</Link>
				</p>
			</footer>,
			document.getElementById('footer'),
		)
	} else {
		return null
	}
}

export default Footer

Why the if check? Because sometimes the React Portal does not behave as expected when there is more than one React Portal. And since we use document.getElementById() when creating a React Portal, I added the if check to check whether or not the document object exists. This fixed the issue for me!

Then, inside Next.js_document.js, I did the following:

import { Html, Head, Main, NextScript } from 'next/document'

export default function Document() {
	return (
		<Html lang="en">
			<Head />
			<body>
				<div className="site-content">
					<Main />
					<NextScript />
					<div id="notifications"></div>
				</div>
				<div id="scroll-step"></div>
				<div id="scroll-top"></div>
				<div id="footer"></div>
			</body>
		</Html>
	)
}

I got away with my previous implementation of the sticky footer by simply manipulating _document.js, but here, and in the future, I will create a React Portal for the sticky footer so as to avoid hydration issues and have clean React JSX markup. All the other divs containing ids point to React Portals as well.

Happy React Portaling!