Tasks and portals in React

Tasks and portals in React

Modals are arguably one of the most common UI patterns in single page app development, and there are many, many ways to implement them. We use React for our rendering layer in our Moneyhub client apps, and this article shows you how we skinned this particular cat.

The problem

We recently had a design change that meant we wanted to be able to perform critical user journeys as a set of discrete tasks. For example, let's say I'm using the Moneyhub client app to manage my various bank accounts, and my financial adviser has set up a new cash ISA on my behalf. They would want to make sure that they can share this cash ISA with me via the Moneyhub app so that I can see its balance. To achieve this, my IFA as a user of our enterprise platform would want to connect to my ISA provider, retrieve my ISA data and then share that data with me.

As you can imagine each of these flows should keep my IFA user updated, giving them feedback and prompting them for input as they progress. The idea isn't so much to handhold through each task, rather to prompt them into making choices that are simple and intuitive. Remember that we're talking about an enterprise user, so they probably have lots of clients that they want to manage. It's really important that we make their job as simple as possible.

Tasks

Once we had identified the tasks that needed to be performed e.g. connecting to an ISA provider, or retrieving client account data, we needed a solution that would give the user the correct outcome as they progress. We decided to model a simple state machine, wrapping its logic inside a React component. In our case this component is known as a `<Task />`.

Each task is very simple, it takes an array of screens to iterate over, as well as an identifier for the current screen (status) that is being displayed. This can be set out simply using the following Pseudo code.

function Screen({name, nextScreen, handler}) {
    return (
        <div onClick={() => handler(nextScreen)}>{name}</div>
    )
}

const screens = {
    screenA: () => (<Screen name="Screen A" nextScreen="Screen B" />),
    screenB: () => (<Screen name="Screen B" nextScreen="Screen C" />),
    screenC: () => (<Screen name="Screen C" nextScreen="Screen A" />),
    loading: () => (<div>Loading...</div>),
}

<Task
    screens={screens}
    status={screens.screenA}
/>

Next we wrap the task, itself, inside a <TaskProvider /> component, that would bind events to progress the user through the screens.

function Screen({name, nextScreen, handler}) {
    return (
        <div onClick={() => handler(nextScreen)}>{name}</div>
    )
}

function Task({screens, status}) {
 return (screens[status])
}

const screens = {
    screenA: (handler) => (<Screen name="Screen A" nextScreen="screenB" handler={handler} />),
    screenB: (handler) => (<Screen name="Screen B" nextScreen="screenC" handler={handler} />),
    screenC: (handler) => (<Screen name="Screen C" nextScreen="screenA" handler={handler} />),
    loading: () => (<div>Loading...</div>),
}

function bindHandlerToScreens(screens, handler) {
  return R.mapObjIndexed((screenFn) => (screenFn(handler)), screens)
}

class TaskProvider extends React.Component {
  constructor(props) {
    super(props)
    this.state = {status: R.head(R.keys(this.props.screens))}
    this.choose = this.choose.bind(this)
  }

  choose(type) {
    // fake XHR latency
    this.setState({status: "loading"})
    setTimeout(() => this.setState({status: type}), 500)
  }

  exit() {
    this.setState({
      status: null,
    })
  }

  render() {
    const {status} = this.state
    const screens = bindHandlerToScreens(this.props.screens, this.choose)
    return (
      <Task
        screens={screens}
        status={status}
      />
    )
  }
}

ReactDOM.render(<TaskProvider screens={screens} />, document.getElementById("container"))

While the example is a little rough around the edges, it shows you the core pattern. Also the screens, the task and handlers are nicely decoupled and broken into separate parts.

This is really useful as it means that we started to separate our components between those that are responsible for arranging data and orchestrating callbacks and actions (Containers), and those that should present content to the user (Presentational). You can read a more detailed explanation of this here : https:[email protected]_abramov/smart-and-dumb-components-7ca2f9a7c7d0
 

Now the real problem

Having a pattern to solve the problem of the user journey through a task however is only part of the solution. We quickly realised that our implementation had to take into account animating between screens, and more importantly rendering those screens as part of a Modal pattern. This meant that we now had to solve two additional problems:

  • Create a set of predefined screens that would compose a single task
  • Render each task outside of the main app

The first part was fairly straight forward. We knew upfront the types of screens that we required, these were divided into two groups: the first would take user input and the second would inform the user of the task’s progress. As the differences between the two groups was so small we decided to abstract a lot of the core functionality for a given screen away into a core Presentational <Modal /> component that would perform all of the common tasks for us e.g. positioning, cancelling a task and animating between screens.

So following on from this architectural division of components, our <TaskProvider /> component is clearly a Container component. Its sole purpose is to orchestrate the mapping of screens to a given task.

As our presentation is abstracted away into the Modal component, our screens then take on the role of Container components. They are very simple stateless components that have a common API. They are only concerned with delegating handlers and data into our Modals, who in turn are responsible for capturing user input (when user input is needed) and displaying a message to the user. A good example would be a screen that prompts the user to take one of two actions. We decided to call this a <Prompt /> component. Here's its API :

function Prompt({content, controls, ...rest}) {
    return (
        <Modal {...rest}>
            {content}
            {controls.map(({text, handler}) => {
                return (
                    <button onClick={handler}>{text}</button>
                )
            })}
        </Modal>
    )
}

<Prompt
    content="Do you want to do this thing?"
    controls={[
        {
            text: "Yes"
            handler: () => console.log("You did the thing!!!")
        },
        {
            text: "No"
            handler: () => console.log("Awww :(")
        }
    ]}
/>

You can see that the actual positioning and managing of the task flow has nothing whatsoever to do with the screen components. The <Modal /> which the <Prompt /> is an abstraction of, is the one that is concerned with positioning etc. At its most basic the <Modal /> component would look something like :

function Modal({children}) {
    return (
        <div className="windowContainer" style={{
                display: "flex",
                position: fixed,
                top: 0,
                left: 0,
                right: 0,
                bottom: 0,
                backgroundColor: "rgba(0, 0, 0, 0.3)",
            }}>
            <div className="content" style={{
                margin: auto,
                padding: 20,
                backgroundColor: "white",
            }}>
                {children}
            </div>
        </div>
    )
}

I've put together a working example to show the container and presentational components work together : http://codepen.io/jeanpaulgorman/pen/RpPjVJ

What becomes clear, is that the actual content of the task screens can be anything that you want it to be, which means that you can render a task into the app at any point that you need. Which leads onto the final problem: as we are using a Modal to render our screens, we really don't want to rely on the css property `position: fixed` to jump the Modal to the top of the DOM stacking order. Really we should continue to utilise the container/presentational pattern so that we can render a Modal outside of the main app, even if that Modal is invoked deep down within the main app.

Not only will this give us a cleaner, simpler DOM structure, it will also allow us to pass our handlers into the Modal from the main app, without having to pass those handlers out of the app.

In short we would want the following :

const myApp = () => {
    <div>
        This is my main app and all of my appy stuff goes here
        <div>Still part of the app</div>
        <Modal>
            I get rendered outside of the main app
        </Modal>
    </div>
}

ReactDOM.render(myApp, document.getElementById("container"))

gives us:

<body>
    <div id="container">
        This is my main app and all of my appy stuff goes here
        <div>Still part of the app</div>
    </div>
    <div id="appended-container">
        I get rendered outside of the main app
    </div>
</body>

This type of relocation of content, or rather transportation of where content is rendered to is known as a Portal within the React community. Looking at the code you can see why. As a developer you define your content and place it into your <Modal /> and the result is transported via a portal to be rendered elsewhere.

So how would the portal actually work? Firstly it should have no boilerplate, it should be a simple wrapping or higher order component that takes children as its main props. If you aren't familiar with high order components (HOCs), you can read more about them here https://facebook.github.io/react/docs/higher-order-components.html, however the basic principle is that they are a function that takes a component as at least one argument and returns a new component that wraps the first, augmenting its behaviour. Here's a simple example of an HOC that logs new props as they come into a component e.g.

function actOnNewProps(WrappedComponent, predicate) {
    return class extends React.Component {
        componentWillReceiveProps(nextProps) {
            predicate(nextProps)
        }

        render() {
            return (
              <WrappedComponent {...this.props} />
           )
        }

    }
}

function SomeComponent({count}) {
    return (<span>{count}</span>)
}

const actionToPerform = ({count}) => {
  if (count % 2)
    console.log("EVEN!!!!")
}

const LogWhenNewCountIsEven = actOnNewProps(SomeComponent, actionToPerform)

ReactDOM.render(<LogWhenNewCountIsEven count={1} />, document.getElementById("container"))

You can see from this example that our higher order component is only adding the minimum functionality to our wrapped component by utilising the React lifecycle methods. That's something that is pretty common for higher order components and something that we'll use now to attach our Modal component to the correct DOM node.

Let's start by creating our HOC and giving it a name.

function createPortal(WrappedComponent) {
    return class reactAppendToDocument extends React.Component {
        render() {
            return(
                <WrappedComponent {...this.props} />
            )
        }
    }
}

Great! Now that we have our basic structure we know how to generate new components that will be appended to the document.

const Portal = createPortal(Modal)

Our usage would then be as follows, and we can pass any props onto this component, safe in the knowledge that they will find their way to our Wrapped Component, in this case the Modal :

<Portal />

Now we need to get our HOC to actually carry out the task of loading our wrapped component into the DOM location of our choosing. To do this, we'll utilise the ReactDOM.render method. This method allows for our component to be attached to a given DOM node, and if that component is then re-rendered, it will carry out the React diffing and shadow DOM rendering that we'd expect of any other react component. Let's add this in.

function createPortal(WrappedComponent) {
    return class extends React.Component {
        componentDidMount() {
          this.uniqueId = Math.random()
          this.update()
        }

        componentDidUpdate() {
          this.update()
        }

        componentWillUnmount() {
          ReactDOM.unmountComponentAtNode(container)
          componentSubtreeRegistry.deleteElement(this.uniqueId)
        }

        update() {
          ReactDOM.render(<WrappedComponent key={this.uniqueId} {...this.props} />, document.getElementById("append-to-container"))
        }

        render() {
            // NOTE: since this is portal component, we need to manage the rendering ourselves
            return null
        }
    }
}

    
function MyModal() {
  return(
    <div>
      I'm a modal
    </div>
  )
}

const Portal = createPortal(MyModal)

function MyApp() {
  return (
    <div>
      My main app is here
      <Portal />
    </div>
  )
}

ReactDOM.render(<MyApp />, document.getElementById("app-container"))

You can see a working example here: http://codepen.io/jeanpaulgorman/pen/MpwqmE

If you open up the dev tools, you'll see that the Modal is being rendered into the `append-to-container` DOM node, completely separate from the main app. This is great as we're now free to style that DOM node as we please, without having to deal with CSS specificity or relying on `position: fixed` to push our Modal to the top of the stack.

However we've still got a few things that would stop this from being really useful. Firstly, we have to hand crank a new DOM node each time we want to attach a different type of element, for example we wouldn't want to load a chat app into the same DOM node as our Modal, things would soon get complicated. Really we want to be able to name each DOM node that we want to render into, and if it's not present, create it.

Secondly, we can currently only render one component at a time. This is fine for many situations, but would soon become unmanageable in a production SPA. Really we need a registry of components that we want to render outside of the main app and we'd want to map each of these to the correct DOM node to load into.

Finally we'll need to tidy up the DOM nodes as we go. There's no point having an empty DOM node sat in place when we have no components to render into it.

So with these things in mind we need to divide this code into three separate parts:

1. Methods to handle DOM reconciliation.

2. A registry of components to render.

3. Methods that will iterate over our registry and render each component into the correct DOM node.

Luckily we've already done this for you. You can checkout the source code here and run a DEMO : https://github.com/jpgorman/react-append-to-body

 

–Written by Jean-Paul Gorman, Software Engineer

 

If you enjoyed reading this post, follow us on Twitter @MoneyhubEnterpr and Linkedin, to keep up to date with our news and insight.