!
LELAND RICHARDSON
Software Engineer · Google
intelligibabble
Cross-Language React
Slide 2
Slide 2 text
Disclaimer
Code is on Github, for example only, will not be maintained
The code that I am presenting today *is* on GitHub, but it is there for academic purposes only. I do not recommend
using it in any production app. I am not planning on maintaining this code.
Slide 3
Slide 3 text
We are all here because of this library known as React. It’s a library for building User Interfaces.
Slide 4
Slide 4 text
It was introduced to the world as a JavaScript library, and I assume for pretty much everyone here, when you think
of “React” you think JavaScript.
Slide 5
Slide 5 text
At first, the only platform React targeted was the web
However, over time some folks realized that React’s declarative approach allowed for a clean separation between application’s implementation and the
underlying platform’s implementation. By leveraging the same React Library in a JavaScript VM hosted with the app…
Slide 6
Slide 6 text
…React Native enabled react development for iOS and Android apps. This architecture meant that although we were able to enable the React ecosystem to
work with native app development, the engineers that this most appealed to were JavaScript engineers.
The end user platforms we were targeting were the web, Android, and iOS, but the developer ecosystems we were targeting was the JavaScript ecosystem.
Slide 7
Slide 7 text
? ?
This means that for a lot of native engineers in these ecosystems, React Native represents not only a different programming language, but an entirely new UI
programming paradigm as well. Android and iOS have both just gotten first class support for two new and exciting programming languages: Kotlin and Swift.
Despite being new languages, the UI programming model these ecosystems largely follow is the same and have a lot of the same problems that React was
built to solve on the web.
Slide 8
Slide 8 text
So I decided to play around with the concept of building React from the ground up in Kotlin and Swift, without any JavaScript dependency whatsoever.
I called this library “Recoil”, and may refer to it as such in this talk, but the name or this particular implementation is not the focus of this talk, and as I
mentioned earlier, it is not a production ready implementation.
Slide 9
Slide 9 text
What does it look like?
Its worth asking what the public API this looks like. React’s public API surface area is actually quite small. The important bits really come down to the
component API and its observable behaviors. First, lets take a look at what this looks like in JavaScript.
Slide 10
Slide 10 text
JavaScript (w/ JSX)
class Button extends Component {
render() {
return (
{this.props.title}
);
}
} The way most of you probably work with the React component API is something like this. React has a class-based
component API which we can subclass, with a required render function where we write this XML-like syntax called JSX.
Slide 11
Slide 11 text
JavaScript (w/ JSX + Flow)
type ButtonProps = {
title: string,
onPress: () => void,
}
class Button extends Component {
render() {
return (
{this.props.title}
);
}
} And some of you may use React with optional Typings via tools like TypeScript or Flow, where it would look
something like this
Slide 12
Slide 12 text
JavaScript (w/ Flow)
type ButtonProps = {
title: string,
onPress: () => void,
}
class Button extends Component {
render() {
return (
h(View, {
onPress: this.props.onPress,
},
this.props.title
)
);
}
} If we desugar that JSX syntax, we would get something like this. A JSX element desugars essentially into a function
invocation. By convention, here we are calling that function “h”.
Slide 13
Slide 13 text
Swift
struct ButtonProps {
let title: String
let onPress: () -> ()
}
class Button: Component {
override func render() -> Element? {
return (
h(View.self, ViewProps(
onPress: props.onPress
)) {
h(props.title)
}
)
}
}
Now this is something that maps rather nicely to Swift. We are able to have a Component base class, with a prop type as a
generic argument, and an overridable render method. Inside of render, we create elements with an “h” function, passing in
the reference of the components we would like to render, along with props the component expects. Additionally, we can
special case children to be returned by a lambda block as a last escaping parameter of h.
Slide 14
Slide 14 text
Kotlin
data class ButtonProps(
val title: String,
val onPress: () -> Unit
)
class Button(props: ButtonProps): Component(props) {
override fun render(): Element? {
return (
h(::View, ViewProps(
onPress = props.onPress
)) {
h(props.title)
}
)
}
}
The Kotlin version is almost identical. we can express components as a generic class with the prop type as a type argument.
One important thing to note is that there is no type here that is platform specific. Everything here is just vanilla swift or
vanilla kotlin, and doesn’t need to import any of the platform View APIs.
Slide 15
Slide 15 text
Wait, but why?
React is a JS library, why would you want to create it in another language?
In order words, why is the React paradigm compelling? What does it have that existing solutions don’t have?
I’m going to talk about a couple of things that make React compelling that have absolutely nothing to do with the underlying
language or platform.
Slide 16
Slide 16 text
Declarative vs Imperative
React is declarative, whereas iOS and Android’s view frameworks are largely imperative. A declarative view framework can
drastically simplify the code of our UI.
Slide 17
Slide 17 text
8 99+
For 0 unread messges, we show just an empty envelope
For 0-99 unread messages, we show an envelope with paper and a badge with the count of unread messages
For 100 or more unreal messages, we want to be a little bit playful and show “99+” in the badge and an “on fire” icon.
Lets consider an example of a small UI. Here we are rendering an inbox icon for a mail or chat application. In this case we
have some application state: how many unread messages the user has.
Slide 18
Slide 18 text
function render(count) {
if (count > 0 && !hasBadge()) {
addBadge();
} else if (count === 0 && hasBadge()) {
removeBadge();
}
if (count > 99 && !hasFire()) {
addFire();
setBadgeText('99+')
} else if (count <= 99 && hasFire()) {
removeFire();
}
if (count > 0 && !hasPaper()) {
removePaper();
} else if (count === 0 && hasPaper()) {
addPaper();
}
if (count <= 99) {
setBadgeText(count.toString())
}
}
To define this UI imperatively, lets say we have
an update function. We receive our new state,
the updated count, and we have to update the
UI. We might implement it something like this.
In this case we see that getting to the *next*
state very much depends on what our
*current* state is.
Slide 19
Slide 19 text
function render(count) {
return (
99} paper={count > 0}>
0}>
{count}
);
}
An identical UI, expressed declaratively, is somewhat simpler. Provided a count, we simple return the state of
the UI that is desired for that count.
While in the imperative version we had to consider what the previous state of the UI was, in the declarative
approach we no longer need to do so, as the underlying framework or runtime is figuring this out for you. In
most cases this is all that is desired.
Slide 20
Slide 20 text
Encapsulation
Another thing about the React paradigm that I think is important is the encapsulation and well defined
ownership it enforces.
Slide 21
Slide 21 text
A component is given props.
A component manages state.
The public API of a component is clearly defined. A component’s public API is the set of props it accepts. A
component is not allowed to change any of the props it is given.
Further, a component can have state. State, unlike props, is managed by the component itself, and ONLY
that component.
Slide 22
Slide 22 text
State changes are localized.
If a state value is not what you expect it to be, the state has to have been modified by the component that
owns it, which is likely the file you are already looking in, and nowhere else. That’s a powerful guarantee.
if you think about it, this is why we have “controlled” components. An input can’t manage its own state or
else no other component would be able to.
Slide 23
Slide 23 text
State flows down via props.
State flows up via callbacks.
The side effect here is that state flows down the component tree as props, and the only way to go “up” the
tree is with callbacks to signal upwards.
Slide 24
Slide 24 text
Composition
Lastly, I want to talk about React’s composition model.
Slide 25
Slide 25 text
Composition vs Inheritance
Many UI libraries are built in languages with an object oriented programming model which tends to push
people towards inheritance as a composition model. React strongly pushes against this and has a
compositional model closer to functional composition.
Slide 26
Slide 26 text
class Input extends View { /* ... */ }
class ValidatedInput extends Input { /* ... */ }
class DateInput extends ValidatedInput { /* ... */ }
class DateRangeInput extends ??? { /* ... */ }
Inheritance
1. To demonstrate the limitations with this inheritance, consider an Input view that subclasses a base View.
2. If we want to augment the behavior of Input to include validation, we might have a Validated input
3. We might further make a DateInput that has some date-based validation defaulted in
4. Now consider what we might do for a DateRangeInput, which has a start date and an end date. We would want to reuse the date-based validation logic of the
DateInput, but we can’t really directly subclass it, since the DateInput assumes a single input, where-as a range has two.
Slide 27
Slide 27 text
function Input({ value, onChange }) { /* ... */ }
function ValidatedInput({ value, onChange, isValid }) {
return
}
function DateInput({ value, onChange }) {
return
}
function DateRangeInput({ value, onChange }) {
return <>
>
}
React’s Composition Model
The react model shines here. Consider
us similarly starting off with an Input,
then a ValidatedInput which renders an
Input, and a DateInput which renders a
ValidatedInput.
When we get to making a
DateRangeInput, there is no identity
crisis for the DateRangeInput
component. We are able to reuse the
date validation logic of the DateInput
without the DateRangeInput have to
assume that its identity is as a single
input.
Slide 28
Slide 28 text
class FancyBox extends View { /* ... */ }
class FancyEditForm extends ??? { /* ... */ }
class EditForm extends FormView { /* ... */ }
class FancyBlogPost extends ??? { /* ... */ }
class BlogPost extends View { /* ... */ }
Inheritance
Another example of inheritance failing as a composition mechanism comes with the idea of containment or decoration.
Consider us having a “FancyBox” view which has no behavior on its own, it is simply meant to wrap other views to decorate them. We would like to reuse this across
several other views - such as a BlogPost and an EditForm view. The inheritance model no longer helps us here. We cannot subclass both BlogPost and FancyBox at the
same time, so the FancyBox view cannot be reused in practice.
Slide 29
Slide 29 text
function FancyBox({ children }) {
return {children}
}
function FancyEditForm(props) {
return
}
function FancyBlogPost(props) {
return
}
function EditForm(props) { /* ... */ }
function BlogPost(props) { /* ... */ }
React’s Composition Model
The react model again shines here. We
are able to create a FancyBlogPost
component which composes the
functionality of the FancyBox component
and the BlogPost component.
Slide 30
Slide 30 text
How does it work?
So perhaps I’ve convinced you that React is a worthwhile idea to replicate in other languages. To talk about the
interesting things that we can do, I want to spend a little bit of time talking about how react works.
Slide 31
Slide 31 text
Render Phase
Reconciliation Phase
Commit Phase
I like to think of React’s runtime in terms of 3 “phases”.
The render phase. The reconciliation phase. And the “commit” phase.
Slide 32
Slide 32 text
render()
reconcile
So when we update our UI, first the render function of our component is called. This returns a lightweight data structure
that essentially encodes which components and views and their attributes should be.
We take that result and compare it to the result that was returned the previous time. This is called reconciliation. If we
encounter more components here, we call render on that component and start the process all over again until there are
no more components to reconcile.
Slide 33
Slide 33 text
render()
reconcile
create #1
commit
operations
create #2
insert #2->#1
insert #3->#1
create #3
move #3, 0
setAttr #1,
‘enabled’, true
Whenever the full tree or subtree is reconciled,
we are able to “commit” the queue of operations
which actually updates the pixels on the screen.
There are various ways we can think of working
through these three phases.
Reconciliation also adds
operations to a queue which are
needed to update the views into
their new state.
Slide 34
Slide 34 text
Constraints drive innovation. One of the only
ways to get out of a tight box is to invent
your way out.
- Jeff Bezos
Slide 35
Slide 35 text
Constraints drive innovation
Sometimes the biggest innovations come when people don’t have the room to do anything but innovate.
Slide 36
Slide 36 text
React’s constraint:
Single Thread.
React’s big constraint is that JavaScript is single threaded. Despite this, we are constantly demanding more and more out
of our JavaScript applications.
Slide 37
Slide 37 text
React’s innovation:
Scheduling.
(aka “Fiber”)
React’s big innovation…. Scheduling. The introduction of the new Fiber architecture in React 16, and the experimental
async features that came along with it, allows for React applications to specify priority of updates. Fiber allows for react
to “pause” the render and reconciliation of a subtree to yield to the JavaScript execution loop, allowing for single
threaded apps to remain highly responsive, even if a given update spans several frames.
Slide 38
Slide 38 text
Threading Model
This innovation was possible in part due to the declarative nature of react that we talked about earlier.
What is interesting to me is the fact that such an innovation never felt absolutely necessary in the context of iOS or Android because those
platforms have multiple threads available. Despite that, engineers targeting these platforms constantly have to think about dropping frames,
and it is extremely difficult to architect your application such that any of your UI logic takes place off of the “main thread”.
The interesting thing here is that the innovation that React brought us in the context of a single thread can apply to multi-threaded
environments just as well, and perhaps even better.
So lets talk a bit about threading, and what kind of threading models React could have in a multi-threaded environment.
Slide 39
Slide 39 text
Render Phase
Reconciliation Phase
Commit Phase
As we discussed earlier, React can be thought of a three different phases.
Slide 40
Slide 40 text
Render Phase
Reconciliation Phase
Commit Phase
Main (UI) Thread
React DOM
In React DOM, or the web, all three of these phases have to happen on the main thread, as that is the only thread available.
Prior to async, all three of these phases had to happen synchronously. With Fiber, they are all still on a single thread, but
render and reconciliation can be paused and yield to other higher priority tasks if needed.
Slide 41
Slide 41 text
Render Phase
Reconciliation Phase
Commit Phase Main UI Thread
React Native
Background Thread
(JavaScript)
React Native has a slightly different threading model. The Render and Reconciliation phase happens in JavaScript which,
unlike web, is NOT the main thread, its a background thread.
Slide 42
Slide 42 text
Render Phase
Reconciliation Phase
Commit Phase Main UI Thread
Recoil (Swift, Kotlin)
Any thread
Recoil is kind of interesting because the only constraint that it has thread-wise is that the commit phase has to be on the
main thread. Render and reconciliation we can choose to be on any thread we want, and in some cases use multiple.
Slide 43
Slide 43 text
commit
render
reconcile
layout
paint
To show more clearly what I’m talking about, consider this color coding for the various phases. I’ve pulled out layout and
paint in this case, since in iOS and Android we have more control over these things.
Slide 44
Slide 44 text
React (Web)
UI
Time
16 ms
commit
render
reconcile
layout
paint
Slide 45
Slide 45 text
React (Native)
UI
Time
16 ms
commit
render
reconcile
layout
paint
Layout
JS
Slide 46
Slide 46 text
Recoil (Swift, Kotlin)
UI
BG #1
BG #2
Time
16 ms
commit
render
reconcile
layout
paint
Slide 47
Slide 47 text
Recoil (Swift, Kotlin)
UI
BG #1
BG #2
Time
16 ms
BG #3
commit
render
reconcile
layout
paint
Slide 48
Slide 48 text
UI
BG #1
BG #2
Time
16 ms
JS
Multi-Language Reconciliation
commit
render
reconcile
layout
paint
Slide 49
Slide 49 text
What else could this enable?
By having react in Swift and Kotlin, targeting iOS and Android, there are a couple of new things that are possible that I think
are pretty interesting.
Slide 50
Slide 50 text
Layout Aware Components
The first is what I like to call “Layout Aware Components”
{ this.el = el; }} className="filler" />
}
if (this.state.rect.width < 300) {
return
}
return
}
}
The solution is to render an empty element to fill up space, add
a ref to it, and then ask the browser what the size of it is in
componentDidMount and rerender.
The problem here is that on the web we have very limited
control over layout. We can only define attributes on DOM
elements that affect layout, but we don’t. Have any hooks into
the actual layout pass.
Every once in a while you will have a component that wants to
know what size it has available in order to render different
things in different layout contexts. Since on first render the
actual DOM elements wouldn’t exist yet, we don’t have any way
of asking what the size of them would be.
Slide 52
Slide 52 text
Layout Aware Components (JS)
class ResponsiveThing extends LayoutAwareComponent {
render(layout: LayoutInfo) {
if (layout.width < 300) {
return
}
return
}
}
What we really want is something like this. If render could get some layout information passed into while the layout pass is
being made, we could handle this use case more directly.
Slide 53
Slide 53 text
Layout Aware Components (Swift)
enum MeasureMode {
case exact
case atMost
case undefined
}
struct LayoutInfo {
let width: Float
let widthMode: MeasureMode
let height: Float
let heightMode: MeasureMode
}
class Button: LayoutAwareComponent {
override func render(layout: LayoutInfo) -> Element? {
// ...
}
}
The API could be exactly this. Yoga has data structures just like this that would make
having a component participate in the layout pass not be that difficult.
Slide 54
Slide 54 text
Layout Aware Components (Kotlin)
enum class MeasureMode {
exact,
atMost,
undefined
}
data class LayoutInfo(
val width: Float,
val widthMode: MeasureMode,
val height: Float,
val heightMode: MeasureMode
)
class Button(props: ButtonProps): LayoutAwareComponent(props) {
override fun render(layout: LayoutInfo): Element? {
// ...
}
}
It would look basically the same for Kotlin.
Slide 55
Slide 55 text
Recoil (Swift, Kotlin)
UI
BG #1
BG #1
Time
16 ms
commit
render
reconcile
layout
paint
Slide 56
Slide 56 text
Host Components
Slide 57
Slide 57 text
class FancyButton extends React.Component {
render() { ... }
}
{
type: FancyButton,
props: ...,
}
Let’s say you have a component called FancyButton. This is called a “composite” component. Which means it just
composes other components together.
When you create a JSX element with the FancyButton, it literally specifies the component as the type. With React DOM, the
only types of components you can define are composite components.
Slide 58
Slide 58 text
{
type: 'div',
props: ...,
}
React DOM does have “host” components though. When we write a JSX element with “div”, the type is a string literal. In
React DOM, these are “host” components.
Slide 59
Slide 59 text
...
React DOM targets the web specifically, so the set of host components are exactly the set of provided DOM elements.
There is no concept of “defining your own” host component for React DOM.
Slide 60
Slide 60 text
{
type: View,
props: ...,
}
{
type: 'RCTView',
props: ...,
}
For React Native, the situation shifts a little bit. In React Native, there are host components just like in React DOM, however
in React Native they are definable.
Slide 61
Slide 61 text
...
For React Native, the situation shifts a little bit. In React Native, there are host components just like in React DOM, however
in React Native they are definable.
Host Component API (Kotlin)
abstract class HostComponent(var props: TProps) {
abstract fun mountComponent(context: Context): TView
abstract fun updateComponent(view: TView, prevProps: TProps)
abstract fun renderChildren(): Element?
}
Despite host components being definable in RN, they have never really been a heavily used feature. I believe that this is
due to a couple of factors:
There is a language divide between host and composite. Composites were defined in JS, and host components in
objective C or Java. Thus, to move from one to the other was a big cognitive shift.
The APIs have always felt second class. It has always felt significantly harder to build a “Host Component” than a
composite component. This doesn’t have to be true, and I’m interested in what kind of things would happen if it wasn’t.
Slide 66
Slide 66 text
Optimizing Host Components
Host Components also offer a unique opportunity for performance gains. I’ve started experimenting with how a react-
like library for iOS and Android could target more than just the traditional UIView and Android base view.
The natural view hierarchy does come at a cost. In React Native there are already optimizations to flatten the view
hierarchy that React Native produces whenever it is possible.
Taking this step a bit further…
Slide 67
Slide 67 text
Host Component API (Swift)
protocol HostLayer {
associatedtype TProps
associatedtype TLayer: CALayer
func mountComponent() -> TView
func updateComponent(layer: TLayer, prevProps: TProps)
func renderChildren() -> Element?
}
It is possible for us to create a HostComponent protocol that doesn’t target UIViews, but instead targets CALayers,
which are considerably more lightweight. For some component’s use cases this may be practical approach and a big
performance boost.
Slide 68
Slide 68 text
Host Component API (Kotlin)
abstract class HostDrawable(var props: TProps) {
abstract fun mountComponent(context: Context): TDrawable
abstract fun updateComponent(drawable: TDrawable, prevProps: TProps)
abstract fun renderChildren(): Element?
}
Similarly, on Android, we could have a host component API that targets Drawables instead of Views.
Slide 69
Slide 69 text
Closing thoughts
The react paradigm itself does not need to be tied to JavaScript.
There is a lot of existing work here: ReasonReact, ComponentKit, Litho.
I’m hopeful there is a lot of work to come. Potentially by someone in this audience!
Slide 70
Slide 70 text
Thank You
twitter.com/intelligibabble
github.com/lelandrichardson