I'm refactoring some old code for an alert widget and am abstracting it into its own component that uses DOM portals and conditional rendering. I want to keep as much of the work inside of this component as I possibly can, so ideally I'd love to be able to expose the Alert component itself as well as a function defined inside of that component triggers the render state and style animations so that no outside state management is required. Something like this is what I'm looking to do:
import Alert, { renderAlert } from '../Alert'
const CopyButton = () => (
<>
<Alert text="Text copied!" />
<button onClick={() => renderAlert()}>Copy Your Text</button>
</>
)
Here's what I currently have for the Alert component - right now it takes in a state variable from outside that just flips when the button is clicked and triggers the useEffect
inside of the Alert to trigger the renderAlert
function. I'd love to just expose renderAlert
directly from the component so I can call it without the additional state variable like above.
const Alert = ({ label, color, stateTrigger }) => {
const { Alert__Container, Alert, open } = styles;
const [alertVisible, setAlertVisible] = useState<boolean>(false);
const [alertRendered, setAlertRendered] = useState<boolean>(false);
const portalElement = document.getElementById('portal');
const renderAlert = (): void => {
setAlertRendered(false);
setAlertVisible(false);
setTimeout(() => {
setAlertVisible(true);
}, 5);
setAlertRendered(true);
setTimeout(() => {
setTimeout(() => {
setAlertRendered(false);
}, 251);
setAlertVisible(false);
}, 3000);
};
useEffect(() => {
renderAlert();
}, [stateTrigger])
const ele = (
<div className={Alert__Container}>
{ alertRendered && (
<div className={`${Alert} ${alertVisible ? open : ''}`}>
<DesignLibAlert label={label} color={color}/>
</div>
)}
</div>
);
return portalElement
? ReactDOM.createPortal(ele, portalElement) : null;
};
export default Alert;
CodePudding user response:
Though it's not common to "reach" into other components and invoke functions, React does allow a "backdoor" to do so.
-
A more React-way to accomplish this might be to abstract the Alert state into an AlertProvider that renders the portal and handles the rendering of the alert and provides the
renderAlert
function via the context.Example:
import { createContext, useContext, useState } from "react"; interface I_Alert { renderAlert: (text: string) => void; } const AlertContext = createContext<I_Alert>({ renderAlert: () => {} }); const useAlert = () => useContext(AlertContext); const AlertProvider = ({ children }: { children: React.ReactElement }) => { const [text, setText] = useState<string>(""); const [alertVisible, setAlertVisible] = useState<boolean>(false); const [alertRendered, setAlertRendered] = useState<boolean>(false); ... const renderAlert = (text: string): void => { setAlertRendered(false); setAlertVisible(false); setText(text); setTimeout(() => { setAlertVisible(true); }, 5); setAlertRendered(true); setTimeout(() => { setTimeout(() => { setAlertRendered(false); }, 251); setAlertVisible(false); }, 3000); }; const ele = <div>{alertRendered && <div> ..... </div>}</div>; return ( <AlertContext.Provider value={{ renderAlert }}> {children} // ... portal ... </AlertContext.Provider> ); };
...
const CopyButton = () => { const { renderAlert } = useAlert(); const clickHandler = () => { renderAlert("Text copied!"); }; return ( <> <button onClick={clickHandler}>Copy Your Text</button> </> ); };
...
function App() { return ( <AlertProvider> ... <div className="App"> ... <CopyButton /> ... </div> ... </AlertProvider> ); }