Using Modals In React.js, The Right Way (ZERO prop drilling)
Tags: react, javascript, typescript, webdev
Published: 2021-09-01 17:04:04

What if controlling a modal was easy as writing the following effect:
```ts
const someModal = useModal()

useEffect(() => {
  if (someModal.isOpen) {
    setTimeout(someModal.close, 1000)
  }
}, [someModal])
```

My name is Itay Schechner, and I’m a growing fullstack develoepr who specializes in back-of-the-frontend code, particularly in React.js.

In this article, I’ll teach you how to write readable, reusable modal utilities.

> NOTE: This article is heavily based on [a previous post](https://dev.to/itays123/using-the-react-context-api-the-right-way-a-time-saver-5c3f) I wrote, explaining usage of the Context API in detail.

What you’ll learn today:

1. usages of the useModal hook
2. The modal component factory
3. Writing readable code with modal factories.

## The Modal Hook

Let’s start with some TypeScript:

```ts
export interface Modal {
  isOpen: boolean;
  open(): void;
  close(): void;
}
```

From that, we understand that each modal will be able to open itself, close itself and “tell” the components and hooks using it if it’s open or not. This hook is relatively easy to implement:

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

You can implement modal logic by using this hook in one of your components, and using a lot of prop drilling. For example:

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

Because we are so used to writing components this way, we don’t recognize the full potential of modals. What if the exports of your modal file would look like this:

```tsx
import LoginModal, { LoginModalOpener } from '../auth/LoginModal';
```

## The Modal Factory

Unlike previous component factories we discussed, this factory will be much more complicated.

Let’s start, again, with some TypeScript, to see the requirements of this factory.

```tsx
export function createModal<T extends object>(
  context: Context<T>,
  name: keyof T,
  openerLabel: string
) { ... }
```

What do we understand from that?

- The function will take a Modal typed field in the context provided, and use it to create the modal
- The function takes an openerLabel field, meaning it will create the opener button as well.
- If we provided an opener, we should be able to provide a closer as well. I want my closer to display an x icon instead of a text, so I’ll upgrade my context action factory first.

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

Now, we can write our modal factory:

```tsx
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()
    ),
  };
}
```

Let’s see how we can use this factory to create clean code. In the example I’ll show you, I will create a Login modal in an authentication context, that is provided for the entire application in the App.tsx file.

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

```

Now, let’s see how SIMPLE our Navbar component becomes:

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

export default function Navbar () {
    return (
        // ... links ommited for bravety
        <LoginModalOpener />
    )
}
```

### Wrapping up

If you think I made a mistake or I could write the post better, please make suggestions.

A project where I used this - 

{% github itays123/partydeck no-readme %}