In part1:
I created a ClientPortal component to render toast messages in a specific DOM element.
✏️https://dev.to/c0xxxtv/reactjs-making-a-simple-toast-message-component-with-usecontext-part1-1l9l
In part2:
Created toast message component to be rendered.
✏️https://dev.to/c0xxxtv/reactjs-making-a-simple-toast-message-component-with-usecontext-and-react-portal-part2-5fmk
In part3, I will define ToastContext using useContext hook.
But Why Using Context??
I have 2 reasons why I decided to use useContext for my toast message feature.
1: Toast message will persist even after page transition
My app has a form that, after a successful submission, navigates the user to a different page. Therefore, the toast message needs to be displayed persistently, even after the page transition.
2: Accessibility Across Components
The function that displays the toast message becomes available in any file that's wrapped by ToastContext.Provider. In many scenarios, my app needs to display the toast message, especially after users perform CRUD operations. They want to see the result of their actions, so I wanted the function that triggers the toast message to be accessible anywhere in my app files."
1: Defining ToastContext
import React, { useState, useContext, useCallback } from 'react';
import { Children } from '@/types/types';
const ToastContext = React.createContext<ToastContextState | undefined>(undefined);
export const ToastProvider: React.FC<Children> = ({ children }) => {
const [isShow, setIsShow] = useState(false);
const [text, setText] = useState('');
const [error, setError] = useState(false);
const showToastMessage = useCallback((text: string, error: boolean | undefined) => {
if (error) {
setError(true);
}
setText(text);
}, []);
React.useEffect(() => {
let timer: NodeJS.Timeout;
if (isShow) {
timer = setTimeout(() => {
setIsShow(false);
}, 5000);
}
return () => clearTimeout(timer);
}, [isShow]);
return <ToastContext.Provider value={{ showToastMessage, isShow, text, error }}>{children}</ToastContext.Provider>;
};
export const useToast = () => {
const context = useContext(ToastContext);
if (context === undefined) {
throw new Error('useToast must be used within a ToastProvider');
}
return context;
};
Create a context in the code below.
const ToastContext = React.createContext<ToastContextState | undefined>(undefined);
Here I make three states.
export const ToastProvider: React.FC<Children> = ({ children }) => {
const [isShow, setIsShow] = useState(false);
const [text, setText] = useState('');
const [error, setError] = useState(false);
This showToastMessage
function below takes two arguments,
one is text
, the message text that will be displayed,
and the other is 'error' which is a boolean value that determine the message type. If the error
is true, the toast message will contain an error icon; otherwise, it will display a success icon."
const showToastMessage = useCallback((text: string, error: boolean | undefined) => {
if (error) {
setError(true);
}
setIsShow(true)
setText(text);
}, []);
Code below is also important.
This useEffect
will set the timer for the message, allowing it to disappear after 5000ms(5s).
Everytime isShow
state changes, it will reset the timer to 5000ms.
React.useEffect(() => {
let timer: NodeJS.Timeout;
if (isShow) {
timer = setTimeout(() => {
setIsShow(false);
}, 5000);
}
return () => clearTimeout(timer);
}, [isShow]);
The ToastProvider returns a Provider component with four values: showToastMessage, isShow, text, and error.
return <ToastContext.Provider value={{ showToastMessage, isShow, text, error }}>{children}</ToastContext.Provider>;
};
Finally, we export a custom hook named useToast. This allows us to access the values provided by the ToastContext in any file that is wrapped by the provider, without having to define the context in each file.
export const useToast = () => {
const context = useContext(ToastContext);
if (context === undefined) {
throw new Error('useToast must be used within a ToastProvider');
}
return context;
};
2: Wrapping the App with the ToastContext.Provider
Then I wrap the whole app with ToastProvider
and place PortalToasty
component in app file.
export default function App({ Component, pageProps }: AppProps) {
const { pathname } = useRouter();
return (
<ApolloProvider client={apolloClient}>
<SessionProvider session={pageProps.session}>
<ToastProvider>
<Header />
<div >
<Component {...pageProps} />
</div>
<PortalToasty/> //place PortalToasty here
</ToastProvider> // wrap the whole app with Toast Provider
</SessionProvider>
</ApolloProvider>
);
}
3: Retrieve Values Using useToast Custom Hook in PortalToasty
In PortalToasty file, retrieve three global values using useToast custom hook to render the toast message conditionally.
const PortalToasty: React.FC = () => {
const { isShow, text, error } = useToast();
The Flow of Toast Message Display
1:addToadtMessage
set the 'text' to whatever text value you pass to it and 'isShow' to true
2: PortalToasty is displayed since the isShow value is set to true.
3: After 5s, isShow is set to false, causing the ToastMessage to disappear.
Use Case
Here's a use case: After a router.push, I added the addToastMessage function to display a toast message:
const { addToastMessage } = useToast(); //retrieve the addToastMessage function using useToadt hook
......
router.push(`/wardrobe/${userId}`);
addToastMessage('Your piece has been successfully registered!');
Conclusion
Personally, I prefer toast messages over large modal messages that require me to click a cancel button to close them. I'm really pleased that I finally took the time to create a custom toast message using useContext and React Portal. It's crucial to have access to the function that triggers the toast message in any file, as you'll likely find yourself using it in many places. That's just how frequently toast messages are used. I hope you find this post helpful!
Top comments (0)