Shindig: React Tab View Controller

Chet Corcos
4 min readNov 24, 2015

--

One of the things I miss about building native iOS apps is the component library. In particular, there are two beastly components that control just about every iOS app: UITabBarController and UINavigationController.

View controllers control which views are being rendered. They aren’t views themselves, but once you realize that React components are just functions, you start building components that simply render other components. They live in the React render tree, but don’t actually show up in the DOM. Some might call these component high-order components (like high-order functions), but don’t like overcomplicating things with fancy words. I like just calling them functions.

The tab view controller is actually relatively simple. Its controlled by its parent through the currentTab prop, and also requires a renderTab prop to render the appropriate tab view. The main responsibility of the tab view controller is to manage the tab instances.

TabVC = createView
mixins: [InstanceMixin]
getInitialState: ->
tabs: @props.instance.tabs or {[@props.currentTab]:{}}
componentWillReceiveProps: (nextProps) ->
# create an instance for the new tab if it doesnt exist
nextTab = nextProps.currentTab
unless @state.tabs[nextTab]
@setState
tabs: React.addons.update @state.tabs,
{[nextTab]: {$set: {}}}
save: ->
tabs: @state.tabs
render: ->
div
className: 'tabvc'
Transition
transitionName: 'tabvc'
@props.renderTab(
@props.currentTab
@state.tabs[@props.currentTab]
)

This controller delegates pretty much everything to its parent. Here’s an example of using TabVC in action.

Tabs = {Events, Users, Profile, FbUsers}App = createView
getInitialState: ->
currentTab: @props.instance.currentTab or 'Events'
save: ->
currentTab: @state.currentTab
renderTab: (name, instance) ->
Tabs[name]({instance})
render: ->
div
className: 'app'
div
className: 'tabbar'
Object.keys(Tabs).map (name) ->
div
key: name
onClick: => @setState({currentTab})
name
div
className: 'content'
TabVC
instance: @childInstance('tabvc')
currentTab: @state.currentTab
renderTab: @renderTab

The TabVC component is just one of the features of my ccorcos:react-ui Meteor package along with React instances and more. Keep reading to learn more!

First Attempt at NavVC

Building a nav view controller is quite similar, but as you will see, we’ll need some more tools to do it well. This section is really just a motivation for the next article and we’ll cover the nav view controller in full later on.

Instead of an object of instances and names, a navigation controller has an array of instances and routes that unshift and pop off of a stack. To push and pop views on and off the stack, we simply pass these functions as arguments to renderScene to get wired up to the child instance.

NavVC = createView
mixins: [InstanceMixin]
getInitialState: ->
transition: @props.instance.state.transition or 'navvc-appear'
stack: @props.instance.state.stack or [{
sceneRoute: @props.rootScene
instance: {}
}]
push: (route) ->
nextStack = React.addons.update @state.stack,
$push:[{
sceneRoute: route
instance: {}
}]
@setState
stack: nextStack
transition: 'navvc-push'
pop: ->
if @state.stack.length is 1
console.warn("Don't' pop off the root!")
else
last = @state.stack.length — 1
nextStack = React.addons.update @state.stack,
$splice:[[last, 1]]
@setState
stack: nextStack
transition: 'navvc-pop'
popFront: ->
@setState
stack: [@state.stack[0]]
transition: 'navvc-pop
save: ->
state: @state
render: ->
{sceneRoute, instance} = @state.stack[@state.stack.length — 1]
pop = @pop if @state.stack.length > 1
popFront = @popFront if @state.stack.length > 1
div
className: @props.className
Transition
transitionName: @state.transition
@props.renderScene(
sceneRoute
instance
@push
pop
popFront
)

There’s a little bit more code this time, but the idea is pretty simple. A route represents a view. This route is typically an object much like the object returned from any client-side router. You can push a view onto the nav stack by calling push with a new route, or you can pop a view off the nav stack by calling pop. In many iOS apps as well, if you click the tab button for the tab you’re currently on, then it will pop all the way to the front of the nav stack, so we include this function as well.

You can use this NavVC much like the TabVC.

Views = {Events, Users, Profile, FbUsers, User, Event}App = createView
renderScene: (route, instance, push, pop, popFront) ->
Views[route]({instance, push, pop, popFront})
render: ->
NavVC
instance: instance
rootScene: 'Events'
renderScene: @renderScene

Its trivial to nest this NavVC within a TabVC as well. Most apps are structured this way in a tab-nav layout. The pop and popFront functions will be undefined if we’re at the rootScreen so the views can determine if they should render a back button or not.

This NavVC works and may be perfect for your layout, but what if you want to trigger push and pop from another component that isn’t a direct descendent of NavVC? If you don’t have any animations in your UI then you probably don’t care if you’re nav bar is inside your view or alongside your NavVC. But if you want to transition from one view to the next and animate the back button within the navbar, then your navbar cannot be a descendant of the NavVC.

At first glance, you may think you can just get away with this:

App = createView
renderScene: (route, instance, push, pop, popFront) ->
@setState({pop})
Views[route]({instance, push})
render: ->
div
className: 'app'
NavBar
pop: @state.pop
NavVC
instance: instance
rootScene: 'Events'
renderScene: @renderScene

However, this will throw an error because you’re effectively calling setState within the 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.

It doesn’t seem to be possible to short circuit the React render tree and send data from one component to a sibling component. This appears to break the unidirectional data flow principle in React. But after fiddling around for a while, I gave birth to another concept — Proxy components. This is the topic of the next article.

--

--