July 9, 2015Open Source · Backend

Announcing XHP-JS: Building efficient user interface components with Hack, React, and XHP

Fred Emmott

As Facebook has grown, there have been three major classes of issues for our UI code:

  • Server-side efficiency: HTTP responses should be quick and easy to compute.
  • Client-side efficiency: Anything computed inside the web browser still needs to be fast so we don't harm perceived load time or battery life.
  • Developer efficiency: Code should be easy to understand, easy to reuse, and easy to refactor.

We solve these issues with several open-source projects: XHP provides a safe, easy-to-use abstraction for server-side UI components; Hack adds convenient support for asynchronous operations; and React provides an efficient client-side UI library.

The open-source XHP library has recently gained support for Hack's async functionality — but purely server-side UIs aren't sufficient for the modern web. Today's release of XHP-JS provides the ability to add client-side logic to XHP components and even to create instances of React components from Hack code.

This post shows how async, React, and XHP can interoperate. If you're not already familiar with them individually, see the overviews in the “Further reading” links below.

Asynchronous XHP

Let's start with something simple: a list of friends' names.

class :fb:friends-list extends :x:element {
  attribute fbid friendsof @required;
  use XHPAsync;

  protected async function asyncRender(): Awaitable<XHPRoot> {
    $friends = await FriendsFetcher::fetch($this->:friendsof);
    $root = <ul />;
    foreach ($friends as $friend) {
      $root->appendChild(<li>{$friend->getPreferedName()}</li>);
    }
    return $root;
  }
}

And in some page:

$fred = 1;
$bob = 2;
$xhp =
  <fb:root>
    <h1>Friends of Fred</h1>
    <fb:friends-list friendsof={$fred} />
    <h1>Friends of Bob</h1>
    <fb:friends-list friendsof={$bob} />
  </fb:root>;
  
echo $xhp->toString();

At this point, the advantages of XHP + async for the creator of :fb:friends-list are:

  • There's no need to worry about escaping — if a friend's preferred name is '<script>alert(“bobby tables”);</script>', that's completely fine.
  • There's no need to worry about how to actually fetch the friends. This is fairly involved: The hypothetical FriendsFetcher class would need to check the given user's friends list visibility, fetch the list of friends, check which of them are visible to the current user, and then create an object for each friend with only those details visible to the current user. There's also no need to decide which version of someone's name to render — for example, if someone has provided his or her name in multiple languages.
  • There's no need to particularly worry about performance. Hack and XHP will automatically parallelize the fetching — so the data fetching for Bob's friends list will happen at the same time as mine, instead of only one at a time.

The page developer doesn't need to know anything other than that he or she wants to render two friends lists.

Extending the component

Suppose that, at some point, we wanted every friends list to include each friend's profile picture and Messenger status. The component would look something like this:

class :fb:friends-list extends :x:element {
  attribute fbid friendsof @required;
  use XHPAsync;

  protected async function asyncRender(): Awaitable<XHPRoot> {
    $friends = await FriendsFetcher::fetch($this->:friendsof);
    $root = <ul />;
    foreach ($friends as $friend) {
      $root->appendChild(
        <li>
            <fb:profile-photo size={PhotoSize::SMALL} user={$friend->getID()} />
            {$friend->getPreferedName()}
            <fb:messenger-status-dot user={$friend->getID()} />
        </li>
      );
    }
    return $root;
  }
}

This does not need any code that instantiates the component to be changed, and is still efficient despite adding additional calls to our photo and messaging services. These will happen in parallel with each other and with any other pending async calls (such as rendering Bob's friends list while also rendering mine).

A common question is how this fits into a model-view-controller architecture. While traditional “template” systems merely render the data provided to them, allowing views to fetch peripheral data actually increases the separation between the layers. In a standard template system, everywhere in the code that wanted to render a friends list would need to fetch profile photos and Messenger status for each friend, when all they care about is rendering a friends list. XHPAsync allows this to be efficiently encapsulated and considered an implementation detail instead of part of the component API.

Making it interactive

We're now efficiently rendering HTML. But nowadays some JavaScript is almost always required. For example, we might want to render a “hovercard” showing more information about a person when you hover the cursor over his or her name:

class :fb:friends-list extends :x:element {
  attribute fbid friendsof @required;
  
  use XHPAsync;
  use XHPJSCall;

  protected async function asyncRender(): Awaitable<XHPRoot> {
    $friends = await FriendsFetcher::fetch($this->:friendsof);
    $root = <ul />;
    foreach ($friends as $friend) {
      $element = <li>{$friend->getPreferedName()}</li>;
      $this->jsCall(
        'Hovercard',
        'registerForElement',
        XHPJS::Element($element),
        $friend->getID(),
      );
      $root->appendChild($element);
    }
    return $root;
  }
}

At some level, the tree will now need to be wrapped in <x:js-scope>...</x:js-scope>, which generates the JavaScript required by its child elements. Assuming this is done in the root element (in this case :fb:root), no changes to users of :fb:friends-list would be needed.

The result can be thought of as equivalent to the following JavaScript:

var Hovercard = (typeof require == 'undefined')
 ? Window['Hovercard']
 : require('Hovercard'); // support Browserify + CommonJS
Hovercard.registerForElement(
  document.getElementById('some_randomly_assigned_id'),
  some_user_id
);

The implementation is more involved:

  • :x:js-scope attaches an empty list of operations to the XHP sub-tree, and blocks its own rendering on the rendering of all its children.
  • jsCall() serializes its parameters and stores them in the list for the current sub-tree.
  • When all the children are rendered (so all the jsCall()s that are going to happen have happened), :x:js-scope outputs all the serialized data as JSON and passes it to the XHPJS JavaScript module, which executes the serialized calls.

It's common to want to pass a reference to an XHP element's DOM node through to JavaScript, and while this is conceptually simple to do by passing through node IDs, this led to bugs in practice: Multiple elements would be given the same ID, or a component author would automatically assign an ID and a component user would specify it manually, leading to conflicting expectations.

We added XHPJS::Element() to address these problems. The return value is a marker object containing a reference to the XHP object, which is special-cased in the serializer: At the end of the scope, a unique random ID attribute is automatically assigned if one wasn't specified, but the existing one is used if provided, and the ID is sent in the JSON, along with a flag indicating that the node should be passed by the client-side unserializer instead of the raw ID. This helps discourage developers from specifying IDs, which reduces the chances of collisions when components are heavily reused within the same page.

React

We have a rapidly growing library of React components, but sometimes we'll want to render the same thing from a page that is mostly static. Rewriting the whole page in React is not always the right decision, but duplicating the rendering code in XHP and React can lead to long-term pain. XHP-JS makes it convenient to construct a thin XHP wrapper around a client-side React element, avoiding both of these problems:

class :fb:friends-list extends :x:element {
  attribute fbid friendsof @required;
  
  use XHPHelpers;
  use XHPReact;
  
  protected function render(): XHPRoot {
    $this->constructReactInstance(
      'FBFriendsList',
      Map { 'friendsof' => $this->:friendsof },
    );
   
    // The React 'FBFriendsList' component is rendered inside this <div /> 
    return <div />;
  }
}

This can also be combined with XHPAsync to prefetch some data, at the cost of slightly weakening encapsulation.

About async

Async is not true multithreading; instead, it allows some of your application PHP code to execute while other parts are waiting for the results of IO operations. HHVM 3.6 and above support asynchronous execution while waiting for any of the following:

  • Mcrouter or memcached
  • cURL
  • PHP streams (except user streams)
  • MySQL
  • Sleeping for a certain amount of time

Including XHP in an existing project

Ideally, the entire UI should be constructed as a single XHP tree — but this isn't practical when trying to extend an existing project, so we provide “escape hatches” to bypass XHP's safety, and take advantage of XHP's features with non-XHP markup:

class OldTemplateXHP implements XHPUnsafeRenderable {
  public function __construct(private OldTemplate $template) {}

  public function toHTMLString(): string {
    return $this->template->render();
  }
}

function XHPIZE(OldTemplate $template): XHPChild {
  return new OldTemplateXHP($template);
}

class :my:xhp-class extends :x:element {
  protected function render(): XHPRoot {
    // ...
    $template = new MyTemplate($a, $b, $c);
    return <div>{XHPIZE($template)}</div>;
  }
}

We've also provided an interface to the popular Bootstrap library that is fully compatible with XHP-JS and XHPAsync:

class :my-thing extends :x:element {
  use XHPAsync;
  
  protected async function asyncRender(): Awaitable<XHPRoot> {
    $items = await Something::fetchList();
    $root = <bootstrap:list-group />;
    foreach ($items as $item) {
      $root->appendChild(
        <bootstrap:list-group:item>
          {$item->getName()}
        </bootstrap:list-group:item>
      );
    }
    return $root;
  }
}

We know that most users of XHP are going to need to integrate JavaScript. We're happy to share our approach with the community. If you have feedback or questions, please reach out on GitHub or on our Facebook Engineering page.

Further reading

Keep Updated

Stay up-to-date via RSS with the latest open source project releases from Facebook, news from our Engineering teams, and upcoming events.

Subscribe
Facebook © 2017