TL;DR - the parent component passes an object of reactive streams to the child component
When working with a web framework, a parent component often needs to pass a property that will change over time into a child component. Typically, the template-based frameworks use special syntax for specifying the parent to child property mapping. Whenever the property value changes in the parent component, it will "magically" update in the child component too.
Let me list how several modern frameworks specify this mapping before showing how reactive frameworks can implement the same feature in more a powerful way.
Mapping properties from parent to child in Angular 1
For example Angular 1 uses directives to map properties.
1 | angular.directive('person', function() { |
Notice that the property scope.firstName
in the child (directive)
is mapped to whatever will be specified as attribute first
in the HTML
of the parent component. The parent component gives the name of its own
property to be passed. In this case the parent's manager.ownName
is mapped
to the child's property firstName
via the HTML tag attribute first
.
1 | <!-- parent template --> |
The communication between the parent thus requires 3 things
- the parent component lists which property of its own (
manager.ownName
) to map to the child component "bind name", in this casefirst
. - the child component looks under this "bind name" and maps the passed
attribute to its own local name
firstName
. - this map is "live" - every time the parent updates
manager.ownName
, the child will have a new value infirstName
. This is implemented perhaps less than efficiently (but very simply) using the dirty checking algorithm during the digest cycle.
One time binding
What if the child is only interested in the initial property value? In that case there is a slightly different notation. The parent component selects to pass only the initial value of its own property.
1 | <!-- parent template --> |
Source: ONE-TIME BINDINGS IN ANGULAR 1.3
To the child directive person
the property still looks the same.
Yet the parent will only pass the first value and will cut the connection.
This improves the performance, yet makes it hard for ensure that the
right initial value is passed. Consider a case when the name is fetched
from the server. If we never assign anything to the expression
manager.ownName
, then the right value is passed to the person
component.
1 | .controller('parent', ($scope, $http) => { |
But if we assign something to the $scope.manager
before HTTP call, for
example for clarity, then that initial value will be passed to the child.
The value received from the HTTP call will become the second and will NOT
make it to the child component person
.
1 | .controller('parent', ($scope, $http) => { |
If we really wanted a flexible communication system, the child component should control when to receive the values from the parent, and when to unsubscribe.
Mapping values in Vue.js
Before we consider this convoluted system an Angular-specific problem, here is the same two way property binding in Vue.js
The custom directive has to declare every expected property, and the parent component has to pass the name of its own property, just like in Angular 1, but with more explicit parts.
1 | Vue.component('person', { |
1 | <!-- parent template --> |
For one time binding (a rare case in Vue.js because there is no dirty checking
performance penalty), there is v-once
directive, but it does not really
give you much in this case.
To solve the HTTP async case and only display the second changed value, one can "watch" the child's data property and keep the track of the change number manually.
1 | // inside the child component |
We have to write a lot more boilerplate code if we want to be flexible with multiple values passed into the component.
Mapping values in React / JSX
React and its template language JSX requires very little custom syntax, yet a "simple" logic like "only the first non-empty string" value requires the imperative "if / else" statement, implemented using boolean expressions.
1 | // inside the parent component |
I find this style a slight improvement, yet also imperative and confusing.
Property passing using reactive programming
The above approaches all used custom syntax to map a property name from the parent object to make it accessible as a property in the child object. Yet, the solutions are opaque and only deal well with "live" data that flows from the parent to the child, without much control over the data flow by the child object.
What I would like to do instead:
- replace framework-specific "magic" properties with clear explicit types.
- make the mapping from the parent property to the child clear and easy to see.
- allow the child component to fully control how it listens to the value updates, or when it stops listening.
Here is my solution (which is similar to the way Cycle.js enabled the communication between its components).
A parent component passes an object of properties it allows the child component to "use". Only it is not a "plain" object, but every property value is a reactive stream.
That's it.
If the child needs every value of the parent's property, it just subscribes to the updates. The code looks something like this:
1 | function person(parent) { |
Here is the entire code with "child", "parent" components and their connection
1 | const Rx = require('rxjs/Rx') |
If the child needs only the first value of the parent's property, it can efficiently get it without any custom template syntax, using regular Rx operator. The change is 1 word.
1 | // child "component" definition |
Let us test this
1 | // parent creates its data |
What if we want to reject all invalid names, and only take the first non-empty string? Pass a predicate to the first() method.
1 | function person(parent) { |
What if we want to be more performant and avoid multiple child messages if the name passed from the parent does not change?
1 | // parent creates its data |
Again, this is a data flow problem, which is solved using a regular Rx operator
1 | function person(parent) { |
Output shows only the changes
1 | person got new name Joe |
Pretty cool, and if the stream passes an immutable data structure, the
effect in the .subscribe
function (printing to the console, rendering
to the DOM) becomes very efficient since it is executed only for if the
data has changed.
Conclusion
With the above approach we have the following advantages
Simplicity
We can avoid framework-specific unnatural textual description of the complex runtime data flow that maps one property to another.
No need to invent <parent name-will-be-camel-cased="foo-bar" />
weird intermediate HTML-compatible grammars.
The explicit library operator s.first()
is always going to be simpler to understand and remember than
the framework-specific ::name="property"
notation.
Power
The child component gets more power over the data flow from the parent
The child can grab every / first / distinct / first passing predicate value from the parent.
Isolation
the parent component can still have control over what gets passed into the child.
For example, the parent component can create a stream of the first value when passing into the child component
1 | // data stream with everything |
Performance
The application can get to the high performance mark because only the running subscriptions are processed
If the child stopped listening after the first passed value by using
.first()
the parent stream will be freed automatically.
Addendum 1
Once one is passing properties as objects full of reactive streams, one can derive a user-friendly API to simplify basic cases. I prefer passing an object with properties because it is simple to destruct. The only improvement would be to automatically wrap primitive values as Observables.
In the code below we recursively walk the properties object and convert any non-object to an observable of a single value.
1 | const R = require('ramda') |
In another abstraction we could delay creating observables until they are
requested by the child component. For example, we can pass an object
that will only create an Observable when the child component calls
.need(<path>)
.
1 | const Rx = require('rxjs/Rx') |
In a real system, the parent's data would already have Observable
values and our .need(<path>)
method would convert them into shared
"hot" streams instead.
Addendum 2
The child component function is isolated from the parent environment, yet they can coexist and communicate via the "in-" stream. Because the child component "picks" the properties it uses by subscribing there is almost no performance penalty in passing more properties than necessary (there is a tiny overhead for creating an idle Observable). I would use the data encapsulation by the parent heuristic when deciding which properties to expose to other components.
Similarly, any component can be isolated from the DOM environment by creating a "wrapper" object that allows one to subscribe to the events. In this case the performance is even better because no one should create unnecessary streams in the first place.
1 | const DOM = { |
Looking at the native DOM as a reactive stream API is a powerful abstraction.
Addendum 3
So far we have only seen the data flow in one direction - from the parent
component function into the child component function. What if we want to
return data from the child component? Notice that in all examples,
we wrote variable name child
and assigned the result of calling
person(parent)
to this variable, yet function person
has never
returned anything!
1 | function person(parent) { |
Since we are dealing with the real world, it makes sense for the child component to return multiple values. The way for a function to return multiple values is to return an Observable.
I prefer to return again an object with individual property values being Observables, to have symmetry with the input object full of reactive streams.
1 | const Rx = require('rxjs/Rx') |
A word of caution
What if you want the two components "talk to each other"? For example, you could wait for the "child" component's reaction before assigning a new manager. When manually pushing events around like this, it is important to remember when to stop and "complete" the stream.
To demonstrate, let us try making a simple Subject
which is an
Observable but allows us to push new events into it.
When the reply from the child component arrives
we will push the second value (and expect the child to reply with the second
reaction text).
1 | // child "component" definition |
Hmm, we cannot have a reactive stream go into a loop, pushing new event
over and over. We need to know when to stop! In this case we never sent
complete
event to the manager.ownName
stream, thus we got into the
infinite loop. We need to send the "complete" event after sending the
data AND we need to do this asynchronously to allow the "complete"
propagate, rather just the data event (getting into the infinite loop
again).
1 | child.reaction.subscribe(s => { |
When connecting streams into loops, it is important to remember that the stream cannot form a cycle, thus there has to be a "breaker circuit" that stops the same event from traveling in circles.
- Mobx is the closest implementation
to the above approach, but with non-Rx terms
(instead using terms like
observer
,reactions
). Definitely check it out. - Cycle.js is a reactive framework that takes modeling everything with streams and isolating components seriously.
- Rxjs is a large library of reactive streams with many many operators. Two good alternatives are xstream and most.