Blueprint: a JUCE Rendering Backend for React.js
In the past three years, I've written several user interfaces in JUCE with the primitives and practices provided therein. In doing so, I found myself missing the features and the way of thinking that I had grown used to in writing React.js applications. In particular, I missed the functional, declarative approach to expressing a user interface– declaring my interface one time, as a function of the application state, and letting the framework take care of navigating from one state to the next. The amount of complexity that that relieves is profound, and that same complexity I found myself wrestling with while building these interfaces in JUCE.
With Blueprint, I tried to strike a balance that would let me use React.js to build my interfaces but still deliver a simple JUCE plugin that can leverage all of the powerful features that JUCE provides for cross platform rendering. More simply, that means writing React, but rendering plain old
juce::Component instances. In this post I'll lay out the high level project architecture that delivers this balance, then end with a brief roadmap of what's coming next, and invite your feedback and contributions.
Blueprint is primarily a composition of a couple major open source projects with a little bit of glue to bring it all together. First and foremost is JUCE itself; Blueprint is actually delivered as a JUCE Module, and so should be thought of as simply an add-on. The process for creating an app or plugin is still just that for making any other JUCE product– you can use the Projucer scaffolding tool and all the features therein.
blueprint::ReactApplicationRoot. This class is a simple
juce::Component with some glue in place to open a
juce::Component, and you can pull it into your application with a simple
setSize in the same way that you interact with any other
Duktape engine, we of course rely on React.js itself, but the next piece of the puzzle is a lesser known package on the NPM repository called react-reconciler. Now, this piece of the architecture is experimental, and therefore unstable, but quite complete and well-implemented. The reconciler is perhaps the most important piece of Blueprint, and to explain it requires a brief review of how React.js works.
When it was introduced, React.js initiated the notion of a virtual DOM. The virtual DOM is React's internal representation of the user interface that you write. That is, when you write an application in React, what you're actually writing is a description of a user interface, which React uses to construct a virtual tree that eventually maps to the rendered view component tree (originally this was just the DOM in a web context). When the application state changes, React computes a new virtual tree based on your implementation, and compares the old tree with the new tree to identify a minimal set of changes that need to be made to the rendered view component tree to reflect the new state of your interface. This process is where the reconciler comes into play: it's through the reconciler that React issues the calls to affect the necessary changes in the rendered view tree to reach the updated state.
Here we come back to JUCE; Blueprint's primary effort is to provide native C++ hooks within the reconciler so that, for example, when React informs the reconciler that we need to add a new child to the rendered view component tree, the reconciler asks the
blueprint::ReactApplicationRoot to simply construct a new
blueprint::View and insert it at the appropriate point in the view tree. Importantly, a
blueprint::View is just a
juce::Component, with all the familiar
mouse*() hooks that a
juce::Component offers, and constructing the tree is little more than a series of
At this point, we've already covered almost the entire project. Specifically we've covered everything from executing a React.js application bundle and providing the backend interface through which
juce::Component instances are constructed and assembled. The last piece then is the propagation of properties from React.js into JUCE, and for that I'll cover specifically the properties that relate to layout within the
blueprint::ReactApplicationRoot's bounds, which brings us to our final dependency.
Under the hood, Blueprint uses Yoga for computing layout and assigning
juce::Component bounds. Yoga provides a highly robust, and extremely fast flexbox implementation, with the usual facilities for absolute and relative, pixel-specific bounds. Part of the reconciliation process is the propagation of properties assigned at the React component level to the underlying view instance. Here we have the assignment of flex properties to view components, following which is a computation of the component bounds and an assignment of those bounds to the
juce::Component instances we created earlier.
With that, we've finished a brief look at the code path that Blueprint implements and covered each of the major dependencies therein. All that's left now is to share what's coming in the immediate future for this framework.
First, I can say that Blueprint is already complete enough, and fast enough, to provide the entire implementation of the user interface for my upcoming Creative Intent plugin, Remnant– a grain delay audio effect plugin (VST, VST3, AU) with two parallel grain engines that feedback into one another. Remnant will be available later this summer, keep an eye out for it!
At present, there are a few major operations ongoing for Blueprint. As it stands, the implementation is fairly rough, and much refactoring and performance optimization is required to make this properly production-ready. Besides that is a formalization and stabilization of the API, including the API for registering custom
juce::Component implementations with the React.js frontend. Lastly, an open question remains around the implementation of an event system. React and React Native have the notion of a SyntheticEvent with event bubbling through the React element heirarchy. Blueprint has a more rudimentary event system in place, and will require some dedicated effort to get it up to par.
There's plenty more to do than that, still, and with this announcement I hope to encourage feedback and active participation to help me bring this framework to the JUCE community and together we can drive it to bigger and better things. I hope you get involved!