在 React.js 中正确使用 Modals(零 prop 钻取)

2025-06-07

在 React.js 中正确使用 Modals(零 prop 钻取)

如果控制模态框就像编写以下效果一样简单会怎样?

const someModal = useModal()

useEffect(() => {
  if (someModal.isOpen) {
    setTimeout(someModal.close, 1000)
  }
}, [someModal])
Enter fullscreen mode Exit fullscreen mode

我的名字是 Itay Schechner,我是一名成长中的全栈开发人员,专门研究前端后端代码,尤其是 React.js。

在本文中,我将教您如何编写可读、可重用的模态实用程序。

注意:本文主要基于我之前写的一篇文章,详细解释了 Context API 的用法。

今天你将学到:

  1. useModal 钩子的用法
  2. 模态组件工厂
  3. 使用模态工厂编写可读代码。

模态钩子

让我们从一些 TypeScript 开始:

export interface Modal {
  isOpen: boolean;
  open(): void;
  close(): void;
}
Enter fullscreen mode Exit fullscreen mode

由此,我们了解到每个模态框都能够自行打开、关闭,并“告知”使用它的组件和钩子它是否处于打开状态。这个钩子相对容易实现:

export default function useModal(): Modal {
  const [isOpen, setOpen] = useState(false);
  return {
    isOpen,
    open() {
      setOpen(true);
    },
    close() {
      setOpen(false);
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

你可以在某个组件中使用此钩子,并大量使用 prop 语句来实现模态逻辑。例如:

export default function Navbar ()  {
    const { isOpen, open, close } = useModal();
    return (
        <nav>
         // ...navigation code
         { isOpen && <Modal close={close} /> }
         <button onClick={open}>Open Modal</button>
        </nav>
    )
}
Enter fullscreen mode Exit fullscreen mode

因为我们太习惯这样编写组件了,所以我们没有意识到模态框的全部潜力。如果你的模态框文件导出如下会怎么样?

import LoginModal, { LoginModalOpener } from '../auth/LoginModal';
Enter fullscreen mode Exit fullscreen mode

模态工厂

与我们之前讨论过的组件工厂不同,这个工厂将更加复杂。

让我们再次从一些 TypeScript 开始,看看这个工厂的要求。

export function createModal<T extends object>(
  context: Context<T>,
  name: keyof T,
  openerLabel: string
) { ... }
Enter fullscreen mode Exit fullscreen mode

我们从中了解到了什么?

  • 该函数将在提供的上下文中获取一个 Modal 类型的字段,并使用它来创建模态框
  • 该函数采用 openerLabel 字段,这意味着它也将创建开启按钮。
  • 如果我们提供了开场白,那么我们也应该能够提供结尾白。我希望结尾白显示一个 x 图标而不是文本,所以我会先升级上下文动作工厂。
type JSXProvider<Props> = (props: Props) => JSX.Element;

export function action<T extends object, Props extends object = {}>(
  label: string | JSXProvider<Props>, 
  context: React.Context<T>,
  consumer: (ctx: T) => void,
) {
  return function ContextAction({ className, ...props }: withClass & Props) {
    const ctx = useContext(context);
    const action = useCallback(() => consumer(ctx), [ctx]);
    return (
      <button onClick={action} className={className}>
        {typeof label === 'string' ? label : label(props as unknown as Props)}
      </button>
    );
  };
}
Enter fullscreen mode Exit fullscreen mode

现在,我们可以编写我们的模式工厂:

export function createModal<T extends object>(
  context: Context<T>,
  name: keyof T,
  openerLabel: string
) {
  return {
    Visible: createWrapper(
      context,
      ctx => (ctx[name] as unknown as ModalHook).isOpen
    ),
    Opener: action(openerLabel, context, ctx =>
      (ctx[name] as unknown as Modal).open()
    ),
    // Clear: A JSXProvider that takes width and height props
    Closer: action(Clear, context, ctx => 
      (ctx[name] as unknown as Modal).close()
    ),
  };
}
Enter fullscreen mode Exit fullscreen mode

让我们看看如何使用这个工厂创建简洁的代码。在我将要展示的示例中,我将在身份验证上下文中创建一个登录模式,该身份验证上下文在 App.tsx 文件中为整个应用程序提供。

// AuthContext.tsx
export default function AuthContextProvider({ children }: Wrapper) {
  // other auth state ommited for bravety
  const loginModal = useModal();

  // effects ommitted for bravety

  return (
    <AuthContextProvider value={{ loginModal, ...anything }}>{ children }</AuthContextProvider>
  )
} 

// LoginModal.tsx

const ModalProvider = createModal(AuthContext, 'loginModal', 'Log In');

export const LoginModalOpener = ModalProvider.Opener;

export default function LoginModal() {
    return (
        <ModalProvider.Visible> // modal is hidden when hook state is hidden
            // Modal UI (i.e dark fixed background, white modal)
            <ModalProvider.Closer />
            <div>
                // form ommited for bravety
            </div>
        </ModalProvider.Visible>
    )
}

// App.tsx

export default function App () {
    return (
        <AuthContextProvider>
            <LoginModal />
            <Navbar />
            // rest of application
        </AuthContextProvider>
    )
}

Enter fullscreen mode Exit fullscreen mode

现在,让我们看看我们的导航栏组件变得多么简单:

import { LoginModalOpener } from '../auth/LoginModal';

export default function Navbar () {
    return (
        // ... links ommited for bravety
        <LoginModalOpener />
    )
}
Enter fullscreen mode Exit fullscreen mode

总结

如果您认为我犯了错误或者我可以写得更好,请提出建议。

我使用过这个的项目 -

GitHub 徽标 itays123 / partydeck

一款很酷的在线纸牌游戏!

文章来源:https://dev.to/itays123/using-modals-in-react-js-the-right-way-zero-prop-drilling-3ah9
PREV
使用 Swagger 和 Nest.js 向您的 REST API 添加实时文档
NEXT
我制作了一个可以在您的应用程序中隐藏敏感信息的反应组件。