20
loading...
This website collects cookies to deliver better user experience
Dialog
Component using React Hooks, Context API, and Styled Components.Dialog
component can violate the DRY (Don't Repeat Yourself) principle, especially if our App has many pages that have Dialog
required interactions. npx create-react-app reusable-dialog
cd reusable-dialog
npx create-react-app reusable-dialog
command will install React, testing libraries, and several other libraries/tools to build a basic modern web app.cd
is the command for "change directory", it will change the working directory from the current directory to "reusable-dialog". npm install --save styled-components
import { createGlobalStyle } from "styled-components";
export const GlobalStyles = createGlobalStyle`
*, *::before, *::after {
box-sizing: border-box;
margin:0;
padding: 0;
}
html,
body {
background: #F3F5FB;
color: #333;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 16px;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
-webkit-overflow-scrolling: touch;
}
button {
border: none;
cursor: pointer;
}
p {
line-height: 1.4em;
}
`;
GlobalStyles
from index.js
and add it to the ReactDOM.render
method as a component. import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import {GlobalStyles} from "./styles/global-styles";
ReactDOM.render(
<React.StrictMode>
<GlobalStyles />
<App />
</React.StrictMode>,
document.getElementById('root')
);
Dialog
using Styled Components.import styled from "styled-components/macro";
export const Container = styled.div`
background: #f7f9fa;
border-radius: 10px;
box-shadow: rgb(100 100 111 / 20%) 0px 7px 29px 0px;
left: 50%;
max-width: 330px;
padding: 1.25em 1.5em;
position: fixed;
transform: translate(-50%, -50%);
top: 50%;
`;
export const Box = styled.div`
display: flex;
justify-content: center;
& button:first-child {
margin-right: 2em;
}
`;
export const Text = styled.p`
color: black;
font-size: 1.1rem;
margin-bottom: 1.5em;
text-align: center;
`;
export const Button = styled.button`
background: ${({variant = "white"})=> variant === "red" ? "#d2342a" :"#f7f9fa"};
border-radius: 20px;
box-shadow: 0 3px 6px rgba(241, 85, 76, 0.25);
color: ${({variant = "white"})=> variant === "red" ? "white" :"#d2342a"};
font-size: 1.2rem;
padding: 0.3em 0;
text-align: center;
transition: background-color 100ms;
width: 100px;
&:hover {
background: ${({variant = "white"})=> variant === "red" ? "#d82d22" :"#f1f1f1"};
}
`;
Dialog
component we create a div element in index.html
to create a portal to render the Dialog
. In this way, our Dialog
component can exist outside of the DOM hierarchy of the parent component, so it will be much easier to use it and customize it.<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<div id="portal"></div>
</body>
Dialog
and add them to build React component and return it using ReactDom.createPortal()
.import React, { useContext } from "react";
import ReactDOM from "react-dom";
import DialogContext from "../../context/dialog";
import { Container, Box, Text, Button } from "./styles/dialog";
function Dialog({ children, ...rest }) {
const { dialog, setDialog } = useContext(DialogContext);
const { isOpen, text, handler, noBtnText, yesBtnText } = dialog;
return ReactDOM.createPortal(
<Container {...rest}>
<Text>Are you really want to do it?</Text>
<Box>
{children}
<Button variant="red">No</Button>
<Button>Yes</Button>
</Box>
</Container>,
document.getElementById("portal")
);
}
export default Dialog;
Dialog
component.DialogContext
and export it.import { createContext } from "react";
const DialogContext = createContext(null);
export default DialogContext;
DialogProvider
to share the logic between components without having to pass props down manually at every level.import { useState } from "react";
import DialogContext from "../context/dialog";
function DialogProvider({ children, ...props }) {
const [dialog, setDialog] = useState({
isOpen: false,
text: "",
handler: null,
noBtnText: "",
yesBtnText:""
});
return (
<DialogContext.Provider value={{ dialog, setDialog }} {...props}>
{children}
</DialogContext.Provider>
);
}
export default DialogProvider;
Dialog
will use the dialog state which includes several state variables:isOpen
is for showing or not showing the Dialog
.
text
is for the text that we show to the user.
handler
is for the handler function that will be called after clicking the "yes" or similar acceptance button.
noBtnText
and yesBtnText
are the texts of the Dialog
buttons.
DialogProvider
, we wrap our Dialog
component with the DialogProvider
to access the dialog
state.import Dialog from "./components/dialog";
import DialogProvider from "./providers/dialog";
function App() {
return (
<DialogProvider>
<Dialog />
</DialogProvider>
);
}
export default App;
dialog
state variables inside of our Dialog
component.Dialog
button clicks and make the button texts customizable.import React, { useContext, useRef } from "react";
import ReactDOM from "react-dom";
import DialogContext from "../../context/dialog";
import { Container, Box, Text, Button } from "./styles/dialog";
function Dialog({ children, ...rest }) {
const { dialog, setDialog } = useContext(DialogContext);
const { isOpen, text, handler, noBtnText, yesBtnText } = dialog;
const resetDialog = () => {
setDialog({ isOpen: false, text: "", handler: null });
};
const handleYesClick = () => {
handler();
resetDialog();
};
const handleNoClick = () => {
resetDialog();
};
if (!isOpen) return null;
return ReactDOM.createPortal(
<Container {...rest}>
<Text>{text}</Text>
<Box>
{children}
<Button onClick={handleNoClick} variant="red">
{noBtnText}
</Button>
<Button onClick={handleYesClick}>{yesBtnText}</Button>
</Box>
</Container>,
document.getElementById("portal")
);
}
export default Dialog;
Dialog
's accessibility, we should add several things to it.import React, { useCallback, useContext, useEffect, useRef } from "react";
import ReactDOM from "react-dom";
import DialogContext from "../../context/dialog";
import { Container, Box, Text, Button } from "./styles/dialog";
function Dialog({ children, ...rest }) {
const { dialog, setDialog } = useContext(DialogContext);
const { isOpen, text, handler, noBtnText, yesBtnText } = dialog;
const btnRef = useRef(null);
const resetDialog = useCallback(() => {
setDialog({ isOpen: false, text: "", handler: null });
}, [setDialog]);
const handleYesClick = () => {
handler();
resetDialog();
};
const handleNoClick = () => {
resetDialog();
};
useEffect(() => {
const { current } = btnRef;
if (current) current.focus();
}, [isOpen]);
useEffect(() => {
const handleKeydown = (e) => {
if (e.key === "Escape") resetDialog();
};
window.addEventListener("keydown", handleKeydown);
return ()=> window.removeEventListener("keydown", handleKeydown);
}, [resetDialog]);
if (!isOpen) return null;
return ReactDOM.createPortal(
<Container role="dialog" aria-describedby="dialog-desc" {...rest}>
<Text id="dialog-desc">{text}</Text>
<Box>
{children}
<Button ref={btnRef} onClick={handleNoClick} variant="red">
{noBtnText}
</Button>
<Button onClick={handleYesClick}>{yesBtnText}</Button>
</Box>
</Container>,
document.getElementById("portal")
);
}
export default Dialog;
useEffect
hooks, first one calls the callback function to focus on the Dialog
button after rendering the Dialog
. This is much more convenient to use the Dialog
buttons, especially for screenreader users. We achieved this using useRef
hook which is the proper way to manipulate and access the DOM
element in React.role
and aria-describedby
WAI-ARIA attributes to improve accessibility.useEffect
hook calls the callback function to add an event listener to the window
object after rendering the Dialog
which is triggered after keydown
event. If the pressed key is Escape
, Dialog
will be closed.Dialog
component is finished, now we can test it.import React, { useContext } from "react";
import DialogContext from "../context/dialog";
function Home() {
const { setDialog } = useContext(DialogContext);
const handleClick = () => {
setDialog({
isOpen: true,
text: 'Are you want to log "Hello World"?',
handler: () => console.log("Hello World"),
noBtnText: "Don't log",
yesBtnText: "Log it",
});
};
return <button onClick={handleClick}>Activate The Dialog</button>;
}
export default Home;
Dialog
and added a handler for the button. After clicking it, our Dialog
has shown.Dialog
buttons are working correctly too.Dialog
component. We can use this Dialog
component for different actions with different texts. Dialog
with the DialogProvider
, and if there is still performance issues, probably using React.memo
will be a good idea. However, for most applications, I think this won't be needed.