nice-modal-react icon indicating copy to clipboard operation
nice-modal-react copied to clipboard

[Request] Improve typing of modal props

Open gprst opened this issue 2 years ago • 7 comments

It seems not to be possible to call NiceModal.show with a strong typing of the called modal props.

Let's say that MyModal has the following props:

type Props = {
  onSubmit: () => void;
  name: string;
};

Then:

import NiceModal from '@ebay/nice-modal-react';
import MyModal from './components/MyModal';

const App = () => {
  const onClick = () => NiceModal.show(MyModal, {
    something: '123', // No error, even though `MyModal` doesn't have a `something` prop
    name: '456',
  }); // No error, even though prop `onSubmit` hasn't been provided
  
  return <button onClick={onClick}>Show modal</button>;
};

It would be nice to have errors when the provided object doesn't match the type of the modal props 🙂

gprst avatar Oct 26 '23 13:10 gprst

If it isn't to automatically generate the type, we should probably be able to pass it as a simple generic parameter right?

jludwiggreenaction avatar Nov 06 '23 18:11 jludwiggreenaction

If it isn't to automatically generate the type, we should probably be able to pass it as a simple generic parameter right?

I wonder if we could get around this by simply overriding the NiceModal.show() function. I haven't used the library yet (I'm just considering it), however I imagine we should be able to do something like this?

const originalShow = NiceModal.show
NiceModal.show = <T>(modal: T, props: React.ComponentProps<T>, ...args) => originalShow(modal, props, ...args)

baxxos avatar Jan 25 '24 14:01 baxxos

We're able to sort of get around this for now by utilizing the satisfies operator like:

NiceModal.show('demo-modal-name', {
  name: "example name prop",
} satisfies DemoModalProps);

However, a better API would be something like:

NiceModal.show<DemoModalProps>('demo-modal-name', {
  name: "example name prop",
});

xenostar avatar Jul 16 '24 00:07 xenostar

I'm using this workaround in the codebase instead of NiceModal.show.

import NiceModal, { NiceModalHocProps } from "@ebay/nice-modal-react";
import { ComponentProps, FC } from "react";

type Props<C extends FC<any>> = Omit<ComponentProps<C>, keyof NiceModalHocProps> & Partial<NiceModalHocProps>;

export function showModal<C extends FC<any>>(component: C, props: Props<C>) {
    return NiceModal.show(component, props);
}

The Props just makes NiceModalHocProps optional in the component props type. Without it, the ComponentProps<C> type will produce props of component C and NiceModalHocProps, so calling showModal without id or other required props from NiceModalHocProps results in an error.

I don't know if there's a cleaner way to make NiceModalHocProps partial without omitting them first.

HasanMothaffar avatar Aug 06 '24 09:08 HasanMothaffar

I thought it made more sense to type out what is required of the modal and what the modal resolves to, when you actually create the modal, so I have a wrapper like this,

export const ActualNiceModal = {
  ...NiceModal,
  create: <Props extends {}, ResolvedValue = void>(fc: FC<Props>) => {
    const storedModal = NiceModal.create(fc);

    return {
      show: (props: Omit<Props, 'id'>): Promise<ResolvedValue> => {
        return NiceModal.show(storedModal, props);
      },
      hide: () => {
        return NiceModal.hide(storedModal);
      },
    };
  },
};

When you create the modal, you define the types rather than when you use the modal. Above changes the API a bit, so you would call .show on the modal.

If you want something more similar to the current API, maybe something like this can work:

type SmartModal<Props extends {}, ResolvedValue = void> = FC<Props>;

export const ActualNiceModal = {
  ...NiceModal,
  show<Props extends {}, ResolvedValue = void>(
    fc: SmartModal<Props, ResolvedValue>,
    props: Omit<Props, 'id'>
  ) {
    return NiceModal.show<ResolvedValue>(fc, props);
  },
  create<Props extends {}, ResolvedValue = void>(
    fc: FC<Props>
  ): SmartModal<Props & NiceModalHocProps, ResolvedValue> {
    return NiceModal.create<Props>(fc);
  },
};

caspergreen avatar Aug 21 '24 14:08 caspergreen

To make the modal component's props optional doesn't make sense to me. Why lose the type safety?

marcusthelin avatar Aug 30 '24 14:08 marcusthelin

To make the modal component's props optional doesn't make sense to me. Why lose the type safety?

I too didn't understand why the modal's component props were made optional. But today I came across this section in the docs https://github.com/eBay/nice-modal-react?tab=readme-ov-file#declare-your-modal-instead-of-register

I guess they made the props optional to allow showing a modal only by its ID. But I think maybe another API could be used for this?

HasanMothaffar avatar Sep 02 '24 09:09 HasanMothaffar

We also have this problem. Is the only way to ensure type safety moving forward to create custom wrappers?

BooleT37 avatar Apr 22 '25 10:04 BooleT37

Ran into the same issue, tried patching the library, was not satisfied, so I forked it and started putting in the changes and cleanups I needed but still I feel like the open source contributions are low and the authors barely maintain it.

I ended up building my own, a simplified version of this, similar API: @stainless-code/react-modal-manager

import { createModal } from "@stainless-code/react-modal-manager";

// Create modal
export const myModal = createModal<{
  title: string;
  message: string;
}>(({ id, close, title, message }) => (
  <div className="modal-overlay">
    <div className="modal-content">
      <h2>{title}</h2>
      <p>{message}</p>
      <button onClick={close}>Close</button>
    </div>
  </div>
));

// Usage in a component
function MyComponent() {
  const handleOpenModal = () => {
    // Open modal with props
    const modalId = myModal.open({
      title: "Hello World",
      message: "This is a modal message!",
    });

    console.log("Modal opened with ID:", modalId);
  };

  return (
    <div>
      <button onClick={handleOpenModal}>Open Modal</button>
      <button onClick={myModal.close}>Close Last Modal</button>
    </div>
  );
}

would love some feedback

SutuSebastian avatar Sep 08 '25 09:09 SutuSebastian