Skip to content

Instantly share code, notes, and snippets.

@mhuebert
Last active November 25, 2016 05:01
Show Gist options
  • Save mhuebert/9880327 to your computer and use it in GitHub Desktop.
Save mhuebert/9880327 to your computer and use it in GitHub Desktop.
React Firebase Mixin

I'm trying to find a good way to use React with Firebase. There are three main things going on here:

  1. A router mixin in my root component matches a path to a Handler component.
  2. the Handler component specifies the data it requires in statics.firebase.
  3. 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment