Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Cross-Language React

Cross-Language React

React as an idea and a paradigm is interesting in its own right, and should not be tethered to its JavaScript roots. Ever consider whether or not React would be possible in other languages? Other platforms? What are the pros and cons of doing so? If we had React in multiple languages, would it make sense to change the architecture of React Native?

Recording (React Amsterdam 2018): https://www.youtube.com/watch?v=eR4LjL1h6cE

Leland Richardson

April 13, 2018
Tweet

More Decks by Leland Richardson

Other Decks in Programming

Transcript

  1. 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.
  2. We are all here because of this library known as

    React. It’s a library for building User Interfaces.
  3. 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.
  4. 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…
  5. …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.
  6. ? ? 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.
  7. 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.
  8. 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.
  9. JavaScript (w/ JSX) class Button extends Component { render() {

    return ( <View onPress={this.props.onPress} > {this.props.title} </View> ); } } 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.
  10. JavaScript (w/ JSX + Flow) type ButtonProps = { title:

    string, onPress: () => void, } class Button extends Component<ButtonProps, null> { render() { return ( <View onPress={this.props.onPress} > {this.props.title} </View> ); } } And some of you may use React with optional Typings via tools like TypeScript or Flow, where it would look 
 something like this
  11. JavaScript (w/ Flow) type ButtonProps = { title: string, onPress:

    () => void, } class Button extends Component<ButtonProps, null> { 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”.
  12. Swift struct ButtonProps { let title: String let onPress: ()

    -> () } class Button: Component<ButtonProps> { 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.
  13. Kotlin data class ButtonProps( val title: String, val onPress: ()

    -> Unit ) class Button(props: ButtonProps): Component<ButtonProps>(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.
  14. 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.
  15. 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.
  16. 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.
  17. 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.
  18. function render(count) { return ( <Envelope fire={count > 99} paper={count

    > 0}> <Badge visible={count > 0}> {count} </Badge> </Envelope> ); } 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.
  19. Encapsulation Another thing about the React paradigm that I think

    is important is the encapsulation and well defined ownership it enforces.
  20. 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.
  21. 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.
  22. 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.
  23. 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.
  24. 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.
  25. function Input({ value, onChange }) { /* ... */ }

    function ValidatedInput({ value, onChange, isValid }) { return <Input ... /> } function DateInput({ value, onChange }) { return <ValidatedInput ... /> } function DateRangeInput({ value, onChange }) { return <> <DateInput value={value.start} ... /> <DateInput value={value.end} ... /> </> } 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.
  26. 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.
  27. function FancyBox({ children }) { return <View style={fancy}>{children}</View> } function

    FancyEditForm(props) { return <FancyBox> <EditForm {...props} /> </FancyBox> } function FancyBlogPost(props) { return <FancyBox> <BlogPost {...props} /> </FancyBox> } 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.
  28. 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.
  29. 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.
  30. 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.
  31. 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.
  32. Constraints drive innovation. One of the only ways to get

    out of a tight box is to invent your way out. - Jeff Bezos
  33. Constraints drive innovation Sometimes the biggest innovations come when people

    don’t have the room to do anything but innovate.
  34. 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.
  35. 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.
  36. 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.
  37. Render Phase Reconciliation Phase Commit Phase As we discussed earlier,

    React can be thought of a three different phases.
  38. 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.
  39. 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.
  40. 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.
  41. 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.
  42. Recoil (Swift, Kotlin) UI BG #1 BG #2 Time 16

    ms commit render reconcile layout paint
  43. Recoil (Swift, Kotlin) UI BG #1 BG #2 Time 16

    ms BG #3 commit render reconcile layout paint
  44. UI BG #1 BG #2 Time 16 ms JS Multi-Language

    Reconciliation commit render reconcile layout paint
  45. 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.
  46. Layout Aware Components The first is what I like to

    call “Layout Aware Components”
  47. Layout Aware Components (JS) class ResponsiveThing extends Component { componentDidMount()

    { this.setState({ rect: this.el.getBoundingClientRect() }); } render() { if (!this.state.rect) { return <div ref={el => { this.el = el; }} className="filler" /> } if (this.state.rect.width < 300) { return <SmallThing /> } return <LargeThing /> } } 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.
  48. Layout Aware Components (JS) class ResponsiveThing extends LayoutAwareComponent { render(layout:

    LayoutInfo) { if (layout.width < 300) { return <SmallThing /> } return <LargeThing /> } } 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.
  49. 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<ButtonProps> { 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.
  50. 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<ButtonProps>(props) { override fun render(layout: LayoutInfo): Element? { // ... } } It would look basically the same for Kotlin.
  51. Recoil (Swift, Kotlin) UI BG #1 BG #1 Time 16

    ms commit render reconcile layout paint
  52. class FancyButton extends React.Component { render() { ... } }

    <FancyButton /> { 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.
  53. <div /> { 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.
  54. <div /> <span /> <img /> <p /> <section />

    <h1 /> <h2 /> <h3 />
 ... 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.
  55. <View /> { 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.
  56. <View /> <Text /> <Image /> <ScrollView /> ... 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.
  57. Host Component API (RN iOS) @interface SomeViewManager : RCTViewManager <TView>

    @end @implementation SomeViewManager RCT_EXPORT_MODULE() - (UIView *)view { // ... } RCT_EXPORT_VIEW_PROPERTY(someProp, BOOL) @end
  58. Host Component API (RN Android) abstract class ViewManager<TView> { abstract

    String getName(); abstract TView createViewInstance(Context context); abstract void setSomeProp(TView view, Object value); }
  59. Host Component API (Swift) protocol HostComponent { associatedtype TProps associatedtype

    TView: UIView func mountComponent() -> TView func updateComponent(view: TView, prevProps: TProps) func renderChildren() -> Element? }
  60. Host Component API (Kotlin) abstract class HostComponent<TProps, TView: View>(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.
  61. 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…
  62. 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.
  63. Host Component API (Kotlin) abstract class HostDrawable<TProps, TDrawable: Drawable>(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.
  64. 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!
  65. function render(count) { return { type: Envelope, props: { fire:

    count > 99, paper: count > 0, children: [ { type: Badge, props: { visible: count > 0, children: count, }, }, ], }, }; }
  66. { type: Envelope, props: { fire: false, paper: true, children:

    [ { type: Badge, props: { visible: true, children: 13, }, }, ], }, }