Slide 1

Slide 1 text

! 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”

Slide 51

Slide 51 text

Layout Aware Components (JS) class ResponsiveThing extends Component { componentDidMount() { this.setState({ rect: this.el.getBoundingClientRect() }); } render() { if (!this.state.rect) { return
{ 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.

Slide 62

Slide 62 text

Host Component API (RN iOS) @interface SomeViewManager : RCTViewManager @end @implementation SomeViewManager RCT_EXPORT_MODULE() - (UIView *)view { // ... } RCT_EXPORT_VIEW_PROPERTY(someProp, BOOL) @end

Slide 63

Slide 63 text

Host Component API (RN Android) abstract class ViewManager { abstract String getName(); abstract TView createViewInstance(Context context); abstract void setSomeProp(TView view, Object value); }

Slide 64

Slide 64 text

Host Component API (Swift) protocol HostComponent { associatedtype TProps associatedtype TView: UIView func mountComponent() -> TView func updateComponent(view: TView, prevProps: TProps) func renderChildren() -> Element? }

Slide 65

Slide 65 text

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

Slide 71

Slide 71 text

function render(count) { return { type: Envelope, props: { fire: count > 99, paper: count > 0, children: [ { type: Badge, props: { visible: count > 0, children: count, }, }, ], }, }; }

Slide 72

Slide 72 text

{ type: Envelope, props: { fire: false, paper: true, children: [ { type: Badge, props: { visible: true, children: 13, }, }, ], }, }

Slide 73

Slide 73 text

UI as data

Slide 74

Slide 74 text

UI = f(state)