ReactTS Dialog Pattern


These days, I find myself writing a lot of TypeScript, mainly React applications. For whatever reason, it’s just not feasible to write full featured single-page web applications in C++. Because of that, and the experience I gained in grade-school building webpages, I end up writing a lot of ReactTS. I’d never consider myself an expert, but I’ve learned my way around it in the last 3 years of working with it.

In a few of the applications I’ve worked on, I’ve needed to work with modal dialogs. Most UI libraries for React offer some type of support for this concept via a Modal component. Typically, this is a component that can have child components, and has a boolean property representing whether it’s shown or not. The configuration in your component looks like this.

interface MyComponentProps { };
interface MyComponentState { modalActive: boolean, ...FormState };


class MyComponent extends React.Component<MyComponentProps, MyComponentState> {

    public constructor(props:MyComponentProps) {
       this.state = { modalActive: false };
    }

    public render() {
        return (
        <>
           <Modal isShown={this.state.modalActive}>
                <Form ... />
                <Button onClick={this.closeModal}>Save</Button>
           <Modal />

           /* rest of component here */
           <Button onClick={this.openModal}>Create</Button>
           
        </>
       );
    }

   private openModal = () => {
       this.setState({modalActive:true});
   }
   private closeModal = () => {
        // do stuff with your modal state.

       this.setState({modalActive:false});
   }
};

This is just a rough sketch of how one would implement a Modal. You can see something like it here. This pattern isn’t bad, but it presents a problem. What if you want more than one dialog on a page? What if your dialog has a lot of state? Sure, you could create a new component, and remove the amount of code in your render function, but you still have to add a lot of state to your component.

React is an insanely powerful UI paradigm. Taking massive advantage of composition, and functional programming, you can build hugely flexible UI systems. If you’re familiar with React, you’re familiar with this. When you follow the rules of controlled and uncontrolled components, and apply this model to your UI, it’s actually crazy how easy React makes it to build applications.

Except – when it comes to modal dialogs.

The power of React comes from composition and the passing down of immutable state. Typically, well structured components will take state from their parent, and notify the parent of a requested change. The parent can then decide to change the state, and this will cascade a re-render of the child components. This is beautiful because it enables us to build different views of the same state. (MVC anyone?) It also allows React to be efficient in how it knows when and what to re-render.

So, what’s the deal with modal dialogs? The problem with a modal dialog, is that its concept doesn’t follow this pattern. A modal dialog, represents a Component outside the traditional hierarchy of the page. It’s not really a parent, nor is it really a child, it’s well, a modal. When we do it the way I’ve described above, we’re really coupling the data of the two components together. It’s why modals like this feel so heavy.

I struggled with this for a while until I started dabbling a lot more with async/await. I thought back to my days messing with WinForms, and how they dealt with dialogs and devised a plan. My theory was that I wanted to work with a dialog, as a self contained unit, and effectively ask it for the users response. Something like this.

public onOpenModal = () => {
    var result = this.dialog_.showDialog();
    // do something with the result.
}

This is much cleaner, because the dialog itself encapsulates it’s own data, and it’s own state of whether it’s shown or not. Allowing our component to be unaware of the contents of the dialog. Only how to act on it’s results. It can’t be that simple, or can it? Obviously the pseudocode above doesn’t work, but we can get mighty close to it with React and async/await. I’ve coined this pattern The Dialog Pattern.


interface MyDialogResult { canceled:boolean, /* other result items */ };

interface MyDialogState { shown:boolean, /*other form state*/ };

class MyDialog extends React.Component<{}, MyDialogState> {
    public constructor(props:any) {
        this.state = { shown:false }; 
    }

    public render() {
        return (
          <Modal isShown={this.state.shown} />
              <Form ... />
              <Button onClick={this.submitDialog}>Save</Button>
              <Button onClick={this.cancelDialog}>Cancel</Button>
          </Modal>
        );
    }

    public showDialog: Promise<MyDialogResult> = async () => {
         await this.setStateAsync({shown:true});
         return new Promise(resolver => this.dialogResult_=resolver);
    }

    private setStateAsync  = (state:any) => { 
        return new Promise(resolver => this.setState(state,resolver));
    }

    private submitDialog = () => {
         if(this.dialogResult_ == null)
             return;
         this.dialogResult_({canceled:false, /* values from state */ });
         this.setState({shown:false});
    }

    private cancelDialog = () => {
         if(this.dialogResult_ == null)
             return;
         this.dialogResult_({canceled:true});
         this.setState({shown:false});
    }
    
    private dialogResult_ ?: (r:MyDialogResult)=>void;

};


class MyComponent extends React.Component<MyComponentProps, MyComponentState> {

    public constructor(props:MyComponentProps) {
       this.state = { modalActive: false };
       this.dialogRef_ = React.createRef<MyDialog>();
    }

    public render() {
        return (
        <>
           <MyDialog ref={this.dialogRef_} />
           /* rest of component here */
           <Button onClick={this.createItem}>Create</Button>
           
        </>
       );
    }

   private createItem = async () => {
       if(this.dialogRef_.current == null)
           return;
         const res = await this.dialogRef_.current.showDialog();
         if(res.canceled) return;
         /* submit dialog result to api or whatever... */


   }
   private dialogRef_ : React.RefObject<MyDialog>;
};

Note: I wrote this without the aid of a compiler, but I think it’s close.

As you can see, this really separates the concerns, and cleans up the MyComponent code. It allows you to encapsulate any Modal logic in it’s own component, as well as makes these items reusable throughout the application.

Usually I have some kind of final words or a summary. But this time I don’t. Hopefully this post reaches someone and helps them out.

As always, stay healthy, and happy coding!

Experience is the name everyone gives their mistakes — Oscar Wilde

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: