Rendering ReactJS Components In AngularJS Using AngularJS Directives
Up until now, I've been digging into ReactJS in an isolated context. At InVision, however, some of the teams are starting to render ReactJS components inside of an AngularJS context. From what people have told me, this seems to be a bit of challenge; so, I wanted to take some time to look at the possible complexities of rendering ReactJS inside of an AngularJS application.
Because AngularJS doesn't know anything about ReactJS, and vice-versa, rendering ReactJS inside of AngularJS requires some sort of a "glue" layer. Luckily, that's exactly what a directive does. In AngularJS, a directive is the glue that binds the view-model to the DOM (Document Object Model). And, in our case, that DOM is going to be managed by a ReactJS component.
This "glue" layer will take care of all of the bi-directional translation that is required when an AngularJS and a ReactJS context intercommunicate. Not only does this pertain to the data being passed back and forth, it also includes the implicit error handling, the $digest life-cycle, and the $parse()-based function invocation provided by AngularJS.
In the following demo, there is a significant amount of code; I needed to make it sufficiently complex in order to examine the various facets of communication. But, the real focus of the code is the bnFriend directive. This is the directive that binds the AngularJS context to the ReactJS context.
Things that you should notice about the AngularJS directive:
The directive attributes are mapped onto the Props object that is passed into the ReactJS render function.
The directive is $watch()'ing some subset of the directive attributes so that it can re-render the ReactJS element when the relevant props change.
The ReactJS element is only ever mounted once during the life-cycle of the directive. Any additional updates are propagated through a subsequent render() call.
The directive wraps each mapped method in a scope.$apply() call in order to leverage the implicit error handling provided by AngularJS.
The directive wraps each mapped method in a scope.$apply() call in order to tell AngularJS that the view-model may have been changed by the ReactJS context (which will trigger a $digest cycle).
The directive proxies the methods provided to the Props object in order to translate the ordered invocation arguments, used by ReactJS, into named arguments that are used by AngularJS to define the "locals" override in the context-based dependency injection.
The directive uses the "$destroy" in order to unmount the ReactJS component.
With that said, here is the code for the demo. The user is provided with a list of friends (managed by AngularJS). When the user clicks on a friend, we render the detail (managed by ReactJS). From within the detail (ReactJS), if the user clicks on a "like", we highlight the other friends with matching interests (AngularJS). All in all, this provides some decent two-way communication:
As I click around through the list of friends and likes, I am logging-out the life-cycle methods in the top-level ReactJS component. This lets us see that the ReactJS component is only instantiated once-per-existence of the detail view and that all switching of friends is handled by props changes:
I know that people have tried to find a way to make this kind of directive glue generic. And, I think if all you do is pass in "data" attributes, it can be generic. But, if you are going to pass methods into the ReactJS context, I don't see how you can keep the glue generic. This feels especially true if the method invocation uses arguments both provided by ReactJS as well as by the context in which the ReactJS directive is invoked. Luckily, providing the glue is fairly light weight and I am not sure how much of a "win" there is in a generic wrapper.
Want to use code from this post? Check out the license.
To each their own.
Is there a compelling reason to implement this? Is it more suited for reuse of external React component in an angular project?
What are the net value use cases?
The only real reason that I can think of is Performance. At the end of the day, ReactJS is still faster at rendering AngularJS 1.x for large sets of data. So, you may find that some particularly complex AngularJS component is starting to suffer as the size of the data grows. Theoretically, replacing that AngularJS view with a ReactJS view would provide a performance increase.
But, if you did not a performance problem (which you likely won't will small-medium size sets of data), then there's no real compelling reason to try to render a ReactJS view in an AngularJS context.
My particular interest in this stems from the fact that, at work, some of our teams are starting to use ReactJS and are doing so in an AngularJS context. So, I need to dig in and think about the bi-directional communication so that I can help the other developers debug things when they go wrong :)
As you write more and more React components into your apps, will you completely replace Angular in the future?
This is not something I intend to do. The team, internally, is a bit split - some people (like myself) start new work in AngularJS; others start new work in ReactJS. Personally, I still favor AngularJS over react as a full end-to-end solution and have not yet seen a reason to abandon AngularJS in favor of learning the ins-and-outs of something new.
Just fixed some issues to adapt to React 15.0.1 and ran it in Plunker:
If you had explained it step by step, that would have been great and reducing number of newLines !
I've been seeking this exact solution for a day and a half! Great demo!
Great solution. This is just what I was looking for. I was going to take a different approach of creating an AngularJS service to abstract the calls to React and ReactDOM. This is a much cleaner approach since directives are better suited to handle the React component's lifecycle, rather than manually doing that in a controller. Your solution also has the added benefit of being about to refer to the directive's "element" directly rather than having to write an HTML element into a template, give it an ID, then using getElementById to find the container.
Thanks again, excellent work!