A short example of render prop component which adds a confirmation step to any React event, like form submit or button click.

You’ve done it zillion times — you have a form which has an action that requires an extra confirmation step. Maybe to encourage user to double check entered data or just inform them politely what’s gonna happen. Here’s a nice (and a little bit hackish, unfortunately) way how to do it using render prop component.

Starting point

Suppose you have a form:

<form onSubmit={handleSubmit}>

// form

</form>

When form is submitted, handleSubmit event handler is called, something happens. Now you want to add a confirmation step. You could probably just create a different handler, which opens a modal and add handleSubmit handler to submit button inside modal:

{isOpen && (

<Dialog>

Are you sure? <button onClick={closeConfirmationModal}>No</button>

<button onClick={handleSubmit}>Yes</button>

</Dialog>

)} <form onSubmit={openConfirmationModal}

// ... form

</form>

… something like this. The only problem is if you want to reuse the same behavior in different component. How to turn it into a reusable component?

Render prop component

Before thinking about implementation, let’s sketch possible usecases.

Our form example could look like this:

<Confirm>

{confirm => (

<form onSubmit={confirm(handleSubmit)}>

// ... form

</form>

)}

</Confirm>

The whole confirmation handling is inside Confirm component and we just pass handleSubmit event handler as a callback to confirm handler.

What’s great that we can use this render prop component to intercept any React event, like button click:

<Confirm>

{confirm => (

<button onClick={confirm(launch)}>Launch!</button>

)}

</Confirm>

The API looks good and clean, so what’s the catch? Pooling of synthetic events.

Implementation

The implementation is straightforward. For a modal we’re going to use @reach/dialog made by Ryan Florence. First we call children with openConfirmationDialog handler and then conditionally render Dialog right next to it:

<React.Fragment>

{this.props.children(this.openConfirmationDialog)} {this.state.open && (

<Dialog>

<h1>{this.props.title}</h1>

<p>{this.props.description}</p> <button onClick={this.hideConfirmationDialog}>Cancel</button>

<button onClick={this.confirm}>OK</button>

</Dialog>

)}

</React.Fragment>

Calling children as a function is a common React pattern. You can think of it as of local high-order component — instead of wrapping the whole component, you just pass it a part of your render method.

We have a render method, let’s start implementing event handlers.

Pooling of SyntheticEvents

If you ever tried to pass React event in async handler, you got a nice error in console that it’s not possible, because React event objects are reused. It means when you try to access the event later than it’s fired, it’s already a completely different event.

It’s well documented and it might be removed in future versions, but for now we have to deal with it.

openDialogModal = callback => event => {

// prevent default event action, e.g: form submission

event.preventDefault() // we need to destructure the event

// and get the current target value (e.g: select value

event = {

...event,

target: { ...event.target, value: event.target.value }

} this.setState({

open: true, // save original callback with event in closure

callback: () => callback(event)

})

}

That’s the hackish part.

If you read the docs, there’s a note:

If you want to access the event properties in an asynchronous way, you should call event.persist() on the event, which will remove the synthetic event from the pool and allow references to the event to be retained by user code.

Unfortunately, event.persist() doesn’t work for reading event.target.value . Not sure why, but even when I don’t get any error or warning about Event being reused, it still returns the current target value. It seems that if you intercept the event which would update the input value and tries to read the value later, it reads it from the current DOM value.

Anyway, back to example.

Remaining handlers

Two missing handlers are much easier.

First one is this.confirm , which should call the original handler with original event and close the modal:

confirm = () => {

this.state.callback()

this.hide()

}

We solved the problem with event in openConfirmationModal so right now we just call the event we stored in closure and hide the modal. Which brings us to the last event, which is just a cleanup of callback and closing the modal:

hide = () => this.setState({ open: false, callback: null })

Example

If you want to try it out, here’s the sandbox: