Shindig: React Proxy Component

Chet Corcos
4 min readNov 24, 2015

--

When I first started working with React, all of my views had a clear and obvious component hierarchy. Each view component would pass props down React render tree to child components. React has a principle for this called unidirectional data flow and its a big reason why React is so fast.

But one problem I’ve run into with this principle is that the React render tree is bound to the same structure as the DOM tree. The flow of data in React is thus bound to the way CSS and HTML allow you to lay out your application. This basically means that information can only flow from larger rectangles to smaller inner rectangles.

One particular example that highlights this issue is a navigation view controller component similar to that of the iOS UINavigationController. This component decides what view to render and allows you to push and pop views off a stack. When there is more than one view on the stack, you’ll typically show a back button in the navigation bar to pop views off the stack.

The way you would typically structure your HTML/CSS for this kind of layout is something like this:

<div class="app">
<div class="navbar">
<button class="back">
back
</button>
<span class="title>
App
</span>
</div>
<div class="content">
<div class="view">
Hello World!
</div>
</div>
</div>

Now the question is where the navigation view controller (NavVC) component goes. The NavVC needs to control the back button. So an obvious solution would be to put the navbar inside the view that the NavVC renders. This works fine until you want to animate.

One aesthetic issue with the push-pop animation is that the navbar will slide over along with the view. Typically, the navbar will remain in position while only the view content slides over. Thus, if you want your user interface to animate nicely, the navbar will need to be a sibling of the view.

So then you let the NavVC render the navbar alongside the view content, but then you have a problem where the view needs to set the title of the navbar. This would break the unidirectional data flow principle because the navbar is not a child of the view. It also makes the NavVC a much less generic component by hardcoding a navbar with the view controller. What if you wanted to control push and pop via swipes or something entirely other than a navbar? If we want the user interface to look and feel right, then we need some way to set the title and the back button from within the NavVC to a non-child component. What to I’ve come up with to address this issue is a neat concept called a Proxy component.

The Proxy Component

A proxy component is essentially a placeholder component that can be rendered to from another component. And what’s really nice about it, is you can tie it right into the render cycle without having to think imperatively. This component basically allows you to two bend your React data flow tree into the DOM tree.

The following example shows you how a View component can set the title of its sibling NavBar component using a proxy.

App = createView
componentWillMount: ->
@TitleProxy = createProxy()
render: ->
div
className: 'app'
NavBar
titleProxy: @TitleProxy
View
titleProxy: @TitleProxy
NavBar = createView
render: ->
div
className: 'navbar'
# A Proxy is just a React component
@props.titleProxy()
View = createView
render: ->
@props.titleProxy.render(span({}, 'Hello World!')
div
className: 'view'
'This is my view'

In terms of data flow, the title is still a child of the View. But in terms of the DOM hierarchy, the title is a child of the NavBar.

My initial approach to this, to the advice that I’ve found online, is to use event emitters. The main thing I don’t like about that approach is you end up writing very imperative code, triggering events in componentWillMount, componentWillUnmount, and anywhere else where state may change. You also run into race conditions when one component is trying to set the title in componentWillMount and another component is trying to clear the title in componentWillUnmount. This can have unexpected results based on the view hierarchy and any CSSTransitionGroups controlling the mounting and unmounting of components. Proxies, on the other hand, are very declarative and don’t have any of these race conditions.

The proxy component simply consists of an event emitter system localized to the Proxy component itself and this avoids global events. A first shot at building this component looked something like this:

createProxy = ->
value = undefined

# create an event emitter system to listen to Proxy.render
listeners = {}
listen = (f) ->
id = Random.hexString(10)
listeners[id] = f
f(value)
{stop: -> delete listeners[id]}
dispatch = (x) ->
value = x
for id, f of listeners
f(x)
Proxy = createView
getInitialState: ->
value: undefined
componentWillMount: ->
@listener = listen (value) =>
@setState({value})
componentWillUnMount: ->
@listener.stop()
render: ->
@state.value or false
Proxy.render = dispatch
return Proxy

This component doesn’t quite work yet. One problem is that we still get the same error of setting state within a render function.

Invariant Violation: setState(…): Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.

We can solve this by deferring the dispatch event until after the render is done. I like doing this with a high-order function. Meteor.defer is just a helper function for a window.setTimeout of zero.

defer = (f) ->
(arg) ->
Meteor.defer ->
f(arg)
createProxy = ->
# ...
Proxy.render = defer dispatch
return Proxy

And now this works! But you’ll notice that this component re-renders on every dispatch, even when its the same exact value. We can optimize this by using React.addons.PureRenderMixin with the Proxy component.

Now that’s basically it! This component is included, among other things, in the ccorcos:react-ui Meteor package. And if you’ve been following along with this series of articles, then you’ve noticed that we now have all of the tools necessary to build a navigation view controller component. This is the topic of the next article.

--

--