Shindig: React Component Instances

Chet Corcos
3 min readNov 24, 2015

--

As I started building Shindig, I realized that the entire view state of my components was lost when I would change to another tab, and then back again. This is especially terrible sometimes. Imagine you’ve specified search fields, scrolled deep into a list, clicked on an item in that list, then pressed back to get back to the list, and all your search fields are gone and you’re back to the top of the list. This is just unacceptable for any quality application.

I started asking around on forums and I kept getting these one-line answers: Flux. While Flux is interesting, especially for handling data from the server, I wasn’t satisfied with it for simply remembering some state and the scrollTop of an element.

Instead of using Flux, I came up with a really simple way of maintaining all this state in a global mutable state object that is passed down through the component hierarchy.

For example, Scroll is a simple component that maintains the scroll position of a list. It checks the instance when it mounts to set the scrollTop and when it unmounts, it mutates the instance to remember the scrollTop for the time the component mounts.

Scroll = createView
componentDidMount: ->
@getDOMNode().scrollTop = @props.instance.scrollTop or 0
componentWillUnmount: ->
@props.instance.scrollTop = @getDOMNode().scrollTop
render: ->
div
className: 'scroll'
@props.children

The parent component can pass instances to its children as a property of its own instance.

EventList = createView
componentWillMount: ->
@props.instance.scroll = @props.instance.scroll or {}
renderEventItem: (event) ->
EventItem({event})
render: ->
Scroll
instance: @props.instance.scroll
@props.events.map(@renderEventItem)

Eventually, the entire state of the app bubbles up to the a single top-level instance.

Meteor.startup ->
instance = {}
React.render(App({instance}), document.body)

Now this is really cool. The entire state of the UI is held in a single variable. You may noticed that live-reload is a pain — every time the page reloads when you write some code, you lose the entire state of you’re UI. Well we can actually serialize and save this data to localStorage in the browser between live-reloads, preserving the state of the UI during development and during hot-code-pushes. We use the same Meteor API as with Session/ReactiveDict.

instance = Meteor._reload.migrationData('react-ui-instance') or {}
Meteor._reload.onMigrate 'react-ui-instance', ->
return [true, instance]

Refactoring

The only bad thing about this pattern is that it involves mutation. Mutating data is an anti-pattern. But sometimes mutation is unavoidable in which case, the best practice is to limit the mutation to a function with an immutable interface. This is exactly what I did in the form of a mixin.

Scroll = createView
mixins: [InstanceMixin]
componentDidMount: ->
@getDOMNode().scrollTop = @props.instance.scrollTop or 0
save: ->
scrollTop: @getDOMNode().scrollTop
render: ->
div
className: 'scroll'
@props.children
EventList = createView
mixins: [InstanceMixin]
renderEventItem: (event) ->
EventItem({event, key:event.id})
render: ->
Scroll
instance: @childInstance('scroll')
@props.events.map(@renderEventItem)

Using the InstanceMixin, we can also save a snapshot of the entire UI every time save is called so we can rewind history! This is pretty cool in theory, but honestly I haven’t found it terribly useful.

This is just one of the features in my Meteor package, ccorcos:react-ui.

I have to admit though, if I were to do this all over again, I would probably just be using Redux. It wasn’t around when I started this project, but its very similar and has a lot more community support behind it with Chrome DevTools plugins, etc.

Now that we have a means of saving component instances between mounts, we can build tab view controller that maintains the scroll position of each view when switching back and forth between tabs. This is the topic of the next article.

--

--