I'm trying to find a good way to use React with Firebase. There are three main things going on here:
- A router mixin in my root component matches a path to a
Handler
component. - the
Handler
component specifies the data it requires instatics.firebase
. - a
FirebaseMixin
handles subscription/unsubscription to Firebase data.
All of this works on the client and server (express middleware is at the bottom)
Data Dependencies
Components use FirebaseMixin
and specify data dependencies in statics.firebase
:
Component = React.createClass
mixins: [FirebaseMixin]
statics:
# Describe the data to supply to this component from Firebase.
# This data structure will be mirrored in 'props'.
firebase: ->
ideas:
ref: new Firebase('https://redacted.firebaseIO.com/test1/ideas')
query: (ref) -> ref.limit(50)
parse: (snapshot) ->
_.chain(snapshot.val())
.pairs()
.map((pair) ->
idea = pair[1]
idea.id = pair[0]
idea.href = "/ideas/"+slugify(idea.title)+"+"+idea.id
idea
)
.reverse().value()
server: true
default: []
Mixin
The mixin should subscribe and unsubscribe to Firebase data when the component mounts/unmounts, and when the route changes.
getRootComponent = (component) ->
while component._owner
component = component._owner
component
@FirebaseMixin =
firebaseSubscribe: (props) ->
owner = getRootComponent(this)
@__firebaseSubscriptions = {}
for path, obj of @type.firebase(props.router)
# Ensure empty prop for each path
owner.props[key] = (owner.props[key] || {}) if props.router.path != @props.router.path
do (path, obj) =>
query = obj.query || (ref) -> ref
queryRef = query(obj.ref)
callback = (snapshot) =>
obj.parse = obj.parse || (snapshot) -> snapshot.val()
value = obj.parse(snapshot)
data = {}
data[path] = value
owner.setProps data
@__firebaseSubscriptions[path] =
ref: queryRef
callback: callback
queryRef.on "value", callback
firebaseUnsubscribe: (props) ->
for path of this.props.firebase
{ref, callback} = @__firebaseSubscriptions[path]
ref.off "value", callback
componentDidMount: ->
@firebaseSubscribe(this.props)
componentWillUnmount: ->
@firebaseUnsubscribe(this.props)
componentWillReceiveProps: (newProps) ->
if newProps.router.path != this.props.router.path
@firebaseUnsubscribe(newProps)
@firebaseSubscribe(newProps)
getDefaultProps: ->
props = {}
for path, obj of this.type.firebase(this.props.router)
props[path] = obj.default if obj.default
props
Router
Matches paths to components, based on https://github.com/unfold/reactive
RouterMixin = @Mixin =
getDefaultProps: ->
router: this.matchRoute(this.props.path || window.location.pathname)
handleClick: (e) ->
if (e.target.tagName == 'A') and e.target.pathname[0] == "/"
e.preventDefault()
this.navigate(e.target.pathname)
handlePopstate: ->
path = window.location.pathname
if this.props.router.path != path
this.setProps router: this.match(path)
componentDidMount: ->
window.addEventListener 'popstate', this.handlePopstate
matchRoute: (path) ->
path += "/" if path[path.length-1] != "/"
for route in (this.routes || [])
route.path += "/" if route.path[route.path.length-1] != "/"
pattern = urlPattern.newPattern route.path
params = pattern.match(path)
if params
return {
path: path
params: params
handler: route.handler
}
navigate: (path, callback) ->
window.history.pushState(null, null, path)
this.setProps({ router: this.matchRoute(path) }, callback)
Routes
This is what my routes.coffee
file looks like.
Home = require("./pages/home")
Writing = require("./pages/writing")
WritingView = require("./pages/writingView")
Photography = require("./pages/photography")
Ideas = require("./pages/ideas")
Edit = require("./pages/edit")
Login = require("./pages/login")
Logout = require("./pages/logout")
routes = [
{ path: "/", handler: Home },
{ path: "/writing", handler: Writing },
{ path: "/writing/:id", handler: WritingView },
{ path: "/ideas", handler: Ideas },
{ path: "/writing/edit/:slug", handler: Edit },
{ path: "/ideas/:id", handler: Edit },
{ path: "/login", handler: Login },
{ path: "/logout", handler: Logout },
{ path: "/seeing", handler: Photography },
]
module.exports = routes
Server-side rendering
Express middleware:
routes = require("../components/routes")
Router = require("../utils/router").create(routes)
# root component:
Layout = require("../components/layout")
NotFoundHandler = require("../components/pages/notFound")
# Begin middleware...
module.exports = (req, res, next) ->
props =
path: url.parse(req.url).pathname
# Use our `Router` to find the appropriate `Handler` component, which contains Firebase data dependencies:
match = Router.matchRoute(props.path)
Handler = match?.handler || NotFoundHandler
firebaseManifest = Handler.firebase?(match) || {}
# Fetch our data from Firebase & put it into props:
fetchFirebase firebaseManifest, (firebaseData) ->
_.extend props, firebaseData
# Create our root component, and render it into HTML:
App = Layout(props)
html = React.renderComponentToString(App)
html += "
<script>
var Layout = React.renderComponent(Components.Layout(#{safeStringify(props)}), document)
</script>
"
res.setHeader('Content-Type', 'text/html')
res.send html