弹出窗口的 React ref 反模式

React ref anti-pattern for Popups

最近我发现自己实现了一个基于 ref 的模式,这似乎违背了 react documentation advice

模式是这样的:

type Callback = () => void;
type CallbackWrapper = {callback : Callback}

interface IWarningPopupRef{
    warn : (callback : Callback) => void;
}

interface IWarningPopupProps{
    warningText : string;
}

const WarningPopup = forwardRef<IWarningPopupRef, IWarningPopupProps>(
    const [show, setShow] = useState(false);
    const [callback, setCallback] = useState<CallbackWrapper | null>(null);
    const warn = (callback : Callback) => {
        setShow(true);
        setCallback({callback});
    }
    const acceptWarning = () => {
        setShow(false);
        setCallback(null);
        if(callback != null) callback.callback();
    }
    useImperativeHandle(ref, () => ({
        warn
    }));
    (props, ref) => {
        return (
            <div style={{
                visibility:(show)?"visible":"hidden"
            }}>
                {props.warningText}
                <button onClick={acceptWarning}>Accept</button>
            </div>
        )
    }
)

const Component : React.FC = props => {
    const warningPopupRef = useRef<IWarningPopupRef>(null);
    const doDangerButton = () => {
        warningPopupRef.current!.warn(() => {
            doDangerAction();
        });
    }
    return (
        <button onClick={doDangerButton}>Dangerous button</button>
        <WarningPopup ref={warningPopupRef} 
            warningText="Warning ! This is a dangerous button !"/>
    )
}

如果我遵循 React 文档建议并将状态提升到父组件,我会这样:

interface IWarningPopupProps{
    warningText : string;
    show : boolean;
    onWarningAccept : () => void;
}

const WarningPopup : React.FC<IWarningPopupProps> = props => {
    return (
        <div style={{
            visibility:(props.show)?"visible":"hidden"
        }}>
            {props.warningText}
            <button onClick={props.onWarningAccept}>Accept</button>
        </div>
    )
} 

const Component : React.FC = props => {
    const [warningPopupShow, setWarningPopupShow] = useState(false);
    const doDangerButton = () => {
        setWarningPopupShow(true);
    }
    const acceptWarning = () => {
        setWarningPopupShow(false);
        doDangerAction();
    }
    return (
        <button onClick={doDangerButton}>Dangerous button</button>
        <WarningPopup warningText="Warning ! This is a dangerous button !"
            show={warningPopupShow}
            onWarningAccept={acceptWarning}/>
    )
}

现在我不执行上述操作,因为我担心抽象泄漏以及我的父组件必须处理创建它以操纵的状态和此弹出状态。

我的理由是弹出窗口是导航流的中断,因此应该在它自己的上下文中处理。

我是不是在用这种(反)模式给未来的自己设下陷阱?

我赞成第二个,更多 "React-y",因为:

  • 您的 parent 保持所示状态是有道理的。在以后的使用中,您可能真的很高兴能够更灵活地控制弹出窗口控制流。
  • 您的原始模式阅读和维护起来要复杂得多,因为您公开了命令式 API,它不太明显并且通常需要额外的文档。相反,道具是简单的标准 React,更可预测和可测试。
  • Refs 强制 parent/user 绕过 React 的生命周期,强制他们在预期的时间检查 ref 是否包含一个元素。

最后,您可以将弹出窗口调整为方便您使用的方式:

const Component : React.FC = props => {
    const [warningPopupShow, setWarningPopupShow] = useState(false);
    return (
        <button onClick={doDangerButton}>Dangerous button</button>
        <WarningPopup warningText="Warning ! This is a dangerous button !"
            show={warningPopupShow}
            onShow={setWarningPopupShow} // Simply separate "shown" updates from
                                         // acceptation action in the popup
            onWarningAccept={doDangerAction}/>
    )
}

简单、实用、惯用。