是否可以公开在 React 函数组件中定义的函数以在其他组件中调用?

Is it possible to expose a function defined within a React function component to be called in other components?

我正在重构警报小部件的一些旧代码,并将其抽象为使用 DOM 门户和条件呈现的自己的组件。我想尽可能多地保留这个组件内部的工作,所以理想情况下我希望能够公开 Alert 组件本身以及该组件内部定义的函数触发渲染状态和样式动画因此不需要外部状态管理。这就是我想要做的事情:

import Alert, { renderAlert } from '../Alert'

const CopyButton = () => (
  <>
    <Alert text="Text copied!" />
    <button onClick={() => renderAlert()}>Copy Your Text</button>
  </>
)

这是我目前拥有的 Alert 组件 - 现在它从外部获取一个状态变量,该变量在单击按钮时翻转并触发 Alert 内部的 useEffect 以触发 renderAlert 函数。我很乐意直接从组件公开 renderAlert,这样我就可以调用它而无需像上面那样添加额外的状态变量。

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;

虽然“进入”其他组件并调用函数并不常见,但 React 确实允许“后门”这样做。

想法是通过 React 引用系统强制公开 renderAlert 函数。

示例:

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

更 React-way 实现此目的可能是将 Alert 状态抽象为 AlertProvider,该 AlertProvider 呈现门户并处理警报的呈现并通过上下文提供 renderAlert 功能。

示例:

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