Welcome to our latest blog post, where we delve into the fascinating world of imperative modals in React. If you're accustomed to our usual deep dives into Expo and related technologies, you're in for a treat. This time, we're shifting our focus a bit to explore an aspect of code architecture that applies universally to any React project, not just Expo or React Native.

In this post, we'll explore how to implement and manage modals imperatively. While our example will be rooted in a React Native application, the principles and techniques we'll cover apply to any React environment. So, whether you're working on a web project or a mobile app, you'll find valuable insights and practical tips here.

Prepare yourselves for a deep dive into code. There will be a lot of reading and dissecting of code snippets as we unravel the intricacies of imperative modals. By the end of this post, you'll have a solid understanding of enhancing your React applications with dynamic and flexible modal handling. Let's get started!

Need a top-notch developer on your project fast? Working to a limited budget? Instantly hire experienced React Native and Expo specialists with our flexible Team Augmentation Service. They’re yours for as long as you need them. Learn more.

Revolutionise your React Modals with imperative techniques

The problem with declarative modals

In React, modal components are typically designed to work declaratively. This means we pass in data using props and retrieve data through callbacks. A typical implementation might look like this:

This example is simple and functional. Before using the modal, we adjust the inputData, and once the user makes their choice(s), we retrieve the data through the callback.

However, what if we need this modal to be called by multiple parts of our code? Where do we store the inputData? Who listens to the callback? This can quickly lead to messy code. Wouldn't it be great if we could do something like this instead?

Let's explore an example to see how things can get messy quickly and how making the modal work imperatively can help us make it reusable and simplify our code.

Partner with Morrow to transform your Expo app into a high-performing, secure, and user-friendly solution.
Fill out our Expo audit form and take the first step towards app excellence.

A simple example with declarative modals

So, let’s say that we have been tasked to do the following:

Effectively, we need to create a Modal with a single TextInput element that reports back the answer from the user. Note that each time we call the modal, the title (the question to the user) is different.

We typically expect to see such an implementation in a straightforward way. Of course, this is not the way to do it, but I have seen many such implementations.

Let’s break this code apart.

First, let's look at the Modal component:

This is a typical modal component where we use React state to manage its visibility (modalVisible), title (modalTitle), and input text (inputText). We also have a callback function, onModalSubmit, to handle the submit action.

Next, let's look at the main content of the page that uses the modal:

This part is quite simple. The real complexity (and potential mess) comes in the state and callback handling code.

Here, we store the state for the modal: modalVisible, modalTitle, and inputText.

We also have the state for the App component that uses the modal to query the user: name, surname, and modalMode.

The following functions handle the modal call and its response: onAskName, onAskSurname, and onModalSubmit.

However, there are some issues with this approach:

  • We have a mix of two states: the internal state of the modal and the state of the modal caller.
  • The onAskName and onAskSurname functions need to know the internals of the modal to prepare it before calling. They set the title and are responsible for showing the modal.
  • The onSubmit callback has its drawbacks. It is responsible for hiding the modal.

The main issue is that the modal has a single callback. We have to use this callback to handle all cases based on the invoker of the modal, which is why we store the caller using the modalMode state. This approach is problematic.

This modal is tightly coupled because all its internal state is managed externally. Adding another 'caller' to the modal isn't easy either. We have to incorporate the caller into this modalMode callback logic, which can quickly become unmanageable.

Let's start refactoring this modal to eliminate these drawbacks.

Refactoring step 1: break out modal code into its own component

The first step is to separate our modal into its own reusable component:

Here's how we incorporate this modal into our App:

By doing this, we've hidden the internal state (inputText) within the modal's implementation. This simplifies things a bit.

However, major drawbacks remain:

First, the modal’s state necessary for communication with the outside world (modalVisible, modalTitle, and onSubmit) is still managed in our main App component. This means we can’t easily relocate the modal. If we needed to move it, all the callers would need to know where its interfacing state resides and adapt accordingly.

Second, the outside world needs to know a lot about how the modal works to call it. They need to set the modalTitle state and toggle the visible state to show and hide the modal.

This breaks encapsulation and is not a good practice.

Now, let's move on and eliminate those drawbacks.

Refactoring step 2: simplify modal’s communication state internally

To address the remaining drawbacks, we want to call:

and have the modal initialise and display itself as needed without us worrying about the details. We can achieve this by leveraging React’s ref.

Here's how we can refactor our modal component:

Let's break down what we've changed here:

First, we moved the modal's state internally:

This means we no longer need to provide these as declarative props to our component. Only onSubmit remains as a prop.

Secondly, we defined a specific function (init) for initialising our modal when it needs to be presented to the outside world:

Additionally, the teardown process is handled internally within the onPress function. This function not only triggers onSubmit to report the user’s feedback but also ensures the modal cleans up (i.e., hides) afterwards.

Finally, we exposed this init functionality using forwardRef and useImperativeHandle from React.

Here's how the caller App uses it:

This approach is a major improvement in our code. Our App component is now much clearer.

However, one main issue remains. The onSubmit callback.

Refactoring step 3: eliminating the callback

Let's consider why we use callbacks in modals. Modals typically ask the user for input and wait for the user's response, making it an asynchronous operation. We use callbacks to handle this, similar to querying a database or server.

How can we eliminate callbacks? By using Promises, as we do when querying servers.

Let's implement this. It's going to be fun!

First, init should return a Promise to the caller, which can be awaited to get the result:

What we've done here is store the resolve function of the promise in an internal ref variable so that other parts of our modal can call it and provide the result to the caller.

Since storing a function directly in ref.current can cause TypeScript to complain, we store it in an object's property called resolver.

(Note: Ignore the complexity of the ref's type. It's just to satisfy TypeScript's compiler.)

Next, we call the resolver function when our user is ready to submit their answer:

Finally, the caller can now await the answer instead of using a callback function. This means the onSubmit prop is no longer needed.

Great! We now have much cleaner and more flexible code.

Here is our final version of the code, just for the reference:

Where do we go from here?

The modal can be declared at the root of our React tree, making it globally available in our App.

Additionally, we can inject its ref.current.init function into a root Context Provider, making it accessible throughout the App and allowing any component to trigger the modal.

Since we can now await the modal's result, we gain the flexibility to handle it in more complex scenarios.

For example, we could trigger two modals as a multi-page form in one go:

Imagine that you can have a multi-page form with complex ‘paths’ based on the answers of each form page. It would be impossible to do with callback modals. But now, with the imperative modals, you treat them like any other asynchronous call.

We hope you found this post useful and that it helps you write cleaner and more efficient code!

Need a top-notch developer on your project fast? Working to a limited budget? Instantly hire experienced React Native and Expo specialists with our flexible Team Augmentation Service. They’re yours for as long as you need them. Learn more.
Want experts on your side to drive forward your project?
Need a second opinion on something?
We’re here to help!
Find out more
a photo of the Morrow team sat at a dinner table
More insights