- Published on
React Portal in Next.js and the Sticky Footer
- Authors
- Name
- interglobalmedia
- @letsbsocial1
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!