2

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;
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
Zach Kaigler
  • 279
  • 5
  • 15
  • 1
    I think you're not thinking in react. https://reactjs.org/docs/thinking-in-react.html you should not expose functions of components. Instead pass props to control a components state. – Matthias Wiedemann Mar 17 '22 at 20:30
  • @MatthiasWiedemann In this case passing all of the state in as props means repeating the necessary code across any component that needs to render an alert so that I can then pass it in, which is not ideal because it makes the codebase very un-DRY. I'd love to keep as much of this functionality internal as possible so all of that state and logic lives in one place - the component that uses it. – Zach Kaigler Mar 17 '22 at 20:52

1 Answers1

4

Though it's not common to "reach" into other components and invoke functions, React does allow a "backdoor" to do so.

The idea is to expose out the renderAlert function imperatively via the React ref system.

Example:

import { forwardRef, useImperativeHandle } from 'react';

const Alert = forwardRef(({ label, color, stateTrigger }, ref) => {
  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]);

  useImperativeHandle(ref, () => ({
    renderAlert,
  }));

  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;

...

import { useRef } from 'react';
import Alert from '../Alert'

const CopyButton = () => {
  const ref = useRef();

  const clickHandler = () => {
    ref.current?.renderAlert();
  };

  return (
    <>
      <Alert ref={ref} text="Text copied!" />
      <button onClick={clickHandler}>Copy Your Text</button>
    </>
  )
};

Edit is-it-possible-to-expose-a-function-defined-within-a-react-function-component-to

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>
  );
}

Edit is-it-possible-to-expose-a-function-defined-within-a-react-function-component-to (forked)

Drew Reese
  • 165,259
  • 14
  • 153
  • 181