Shopify is one of the world's largest e-commerce platforms. With millions of merchants worldwide, we support an increasingly diverse set of use cases, and we wouldn't be successful at it without our developer community. Developers build apps that add immense value to Shopify and its merchants, and solve problems such as marketing automation, sales channel integrations, and product sourcing.
In this post, we will take a deep dive into the latest generation of our technology that allows developers to extend Shopify’s UI. With this technology, developers can better integrate with the Shopify platform and offer native experiences and rich interactions that fit into users' natural workflow on the platform.
To put the technical challenges into context, it's important to understand our main objectives and requirements:
- The user experience of 3rd party extensions must be consistent with Shopify's native content in terms of look & feel, performance, and accessibility features.
- Developers should be able to extend Shopify using standard technologies they are already familiar with.
- Shopify needs to run extensions in a secure and reliable manner, and prevent them from negatively impacting the platform (naively or maliciously).
- Extensions should offer the same delightful experience across all supported platforms (web, iOS, Android).
With these requirements in mind, it's time to peel the onion.
At the heart of our solution is a technique we call remote rendering. With remote rendering, we separate the code that defines the UI from the code that renders it, and have the two communicate via message passing. This technique fits our use case very well because extensions (code that defines UI) are typically 3rd party code that needs to run in a restricted sandbox environment, while the host (code that renders UI) is part of the main application.
Communication between an extension and a host is done via a
Remote rendering gives us the flexibility we need, but it also introduces non-trivial technical challenges such as defining an efficient message-passing protocol, implementing function calls using message passing (aka remote procedure call), and applying UI updates in a performant way. These challenges (and more) are tackled by
remote-ui, an open-source library developed at Shopify.
Let's take a closer look at some of the fundamental building blocks that
remote-ui offers and how these building blocks fit together.
At the lower level, the
@remote-ui/rpc package provides a powerful remote procedure call (RPC) abstraction. The key feature of this RPC layer is the ability for functions to be passed (and called) across a
postMessage interface, supporting the common need for passing event callbacks.
@remote-ui/rpc introduces the concept of an endpoint for exposing functions and calling them remotely. Under the hood, the library uses
Proxy objects to abstract away the details of the underlying message-passing protocol.
It's also worth mentioning that
remote-ui’s RPC has very smart automatic memory management. This feature is especially useful when rendering UI, since properties (such as event handlers) can be automatically retained and released as UI component mount and unmount.
After RPC, the next fundamental building block is the
RemoteRoot which provides a familiar DOM-like API for defining and manipulating a UI component tree. Under the hood,
RemoteRoot uses RPC to serialize UI updates as JSON messages and send them to the host.
For more details on the implementation of
RemoteRoot, see the documentation and source code of the
The "opposite side" of a
RemoteRoot is a
RemoteReceiver. It receives UI updates (JSON messages sent from a remote root) and reconstructs the remote component tree locally. The remote component tree can then be rendered using native components.
RemoteReceiver we are very close to having an implementation of the remote rendering pattern. Extensions can define the UI as a remote tree, and that tree gets reconstructed on the host. The only missing thing is for the host to traverse the tree and render it using native UI components.
remote-ui provides a number of packages that make it easy to convert a remote component tree to a native component tree. For example, a
DomReceiver can be initialized with minimal configuration and render a remote root into the DOM. It abstracts away the underlying details of traversing the tree, converting remote components to DOM elements, and attaching event handlers.
In the snippet above, we create a receiver that will render the remote tree inside a DOM element with the id
container. The receiver will convert
LineBreak remote components to
br DOM elements, respectively. It will also automatically convert any prop starting with
on into an event listener.
For more details, check out this complete standalone example in the
Integration with React
DomReceiver provides a convenient way for a host to map between remote components and their native implementations, but it’s not a great fit for our use case at Shopify. Our frontend application is built using React, so we need a receiver that manipulates React components (instead of manipulating DOM elements directly).
@remote-ui/react package has everything we need: a receiver (that receives UI updates from the remote root), a controller (that maps remote components to their native implementations), and the
RemoteRenderer React component to hook them up.
There's nothing special about the component implementations passed to the controller; they are just regular React components:
However, there's a part of the code that is worth taking a closer look at:
// Run 3rd party script in a sandbox environment
// with the receiver as a communication channel ...
When we introduced the concept of remote rendering, our high-level diagram included only two boxes, extension and host. In practice, the diagram is slightly more complex.
The sandbox, an additional layer of indirection between the host and the extension, provides platform developers with more control. The sandbox code runs in an isolated environment (such as a web worker) and loads extensions in a safe and secure manner. In addition to that, by keeping all boilerplate code as part of the sandbox, extension developers get a simpler interface to implement.
Let's look at a simple sandbox implementation that allows us to run 3rd party code and acts as “the glue” between 3rd party extensions and our host.
The sandbox allows a host to
load extension code from an external URL. When the extension is loaded, it will
register itself as a callback function. After the extension finishes loading, the host can
render it (that is, call the registered callback).
Arguments passed to the
render function (from the host) provide it with everything it needs.
remoteChannel is used for communicating UI updates with the host, and
api is an arbitrary object containing any native functionality that the host wants to make available to the extension.
Let's see how a host can use this sandbox:
In the code snippet above, the host makes a
setTitle function available for the extension to use. Here is what the corresponding extension script might look like:
Notice that 3rd party extension code isn't aware of any underlying aspects of RPC. It only needs to know that the
api (that the host will pass) contains a
Implementing a Production Sandbox
The implementation above can give you a good sense of our architecture. For the sake of simplicity, we omitted details such as error handling and support for registering multiple extension callbacks.
importScripts) are made unavailable and others are replaced with safer versions (such as
fetch, which is restricted to specific domains). Also, the sandbox script itself is loaded from a separate domain so that the browser provides extra security constraints.
Finally, to have cross-platform support, we implemented our sandbox on three different platforms using web workers (web), web views (Android), and JsCore (iOS).
We are truly excited about the potential we’re unlocking, and we also know that there's a lot of work ahead of us. Our plans include improving the experience of 3rd party developers, supporting new UI patterns as they come up, and making more areas of the platform extensibile.
Joey Freund is a manager on the core extensibility team, focusing on building tools that let Shopify developers extend our platform to make it a perfect fit for every merchant.
Wherever you are, your next journey starts here! If building systems from the ground up to solve real-world problems interests you, our Engineering blog has stories about other challenges we have encountered. Intrigued? Visit our Engineering career page to find out about our open positions and learn about Digital by Default.