Frage

I have buttons rendered with ReactJS, with their onClick bound to a function that is passed in as a prop. When a button is clicked, it invokes a function, which sets a pushState then renders a new component. That works fine, but after going back to the first page, the button stops responding to the click.

// _init.js.coffee
window.TD or= {}

# Rendering
render_fan = ->
  window.history.pushState({}, "Fan Demo | Tunetap", '/fan')
  render('fan')
render_artist = ->
  window.history.pushState({}, "Artist Demo | Tunetap", '/artist')
  render('artist')
render_venue = ->
  window.history.pushState({}, "Venue Demo | Tunetap", '/venue')
  render('venue')
render = (arg) ->
  target = document.getElementById('content')
  role_children = { render_fan: render_fan, render_artist: render_artist, render_venue: render_venue }
  if arg == 'root'
    React.renderComponent(TD.Root(role_children), target)
  else if arg == 'fan'
    React.renderComponent(TD.FanRoot(role_children), target)
  else if arg == 'artist'
    React.renderComponent(TD.ArtistRoot(role_children), target)
  else if arg == 'venue'
    React.renderComponent(TD.VenueRoot(role_children), target)
  else render_404()
render_404 = -> React.renderComponent(TD.Error404(), document.getElementById('content'))

# Routing
pathname = window.location.pathname.slice(1)
if pathname.length < 1
  render('root')
else if pathname == 'fan' || pathname == 'artist' || pathname == 'venue'
  render(pathname)
else render_404()

// root.js.coffee
window.TD or= {}
TD.Root = React.createClass
  render: ->
    React.DOM.div
      className: 'container'
      children: [
        React.DOM.div
          className: 'row page-header'
          children: [
            React.DOM.h1
              children: 'Tunetap demo'
            React.DOM.p
              children: 'Select a role to get started'
          ]
        React.DOM.div
          className: 'row'
          children: [
            React.DOM.div
              className: 'col-sm-4'
              children: 
                React.DOM.button
                  className: 'btn btn-default col-sm-10 col-sm-offset-1'
                  children: 'Fan'
                  onClick: @props.render_fan
            React.DOM.div
              className: 'col-sm-4'
              children: 
                React.DOM.button
                  className: 'btn btn-default col-sm-10 col-sm-offset-1'
                  children: 'Artist'
                  onClick: @props.render_artist
            React.DOM.div
              className: 'col-sm-4'
              children: 
                React.DOM.button
                  className: 'btn btn-default col-sm-10 col-sm-offset-1'
                  children: 'Venue'
                  onClick: @props.render_venue
          ]
      ]
TD.FanRoot = React.createClass
  render: ->
    React.DOM.p
      children: 'Fan root'
TD.ArtistRoot = React.createClass
  render: ->
    React.DOM.p
      children: 'Artist root'
TD.VenueRoot = React.createClass
  render: ->
    React.DOM.p
      children: 'Venue root'
War es hilfreich?

Lösung

Thanks for the live demo. I've determined that this is a problematic interaction between React 0.8 and Rails's Turbolinks. Here's a summary of what's happening:

  1. On initial page load, the JavaScript file is evaluated, which loads jQuery, jquery-ujs, Turbolinks, React, and your app code. The initial render code is run, which renders the Root component into #content. Because React relies on event delegation for performance, it attaches its event handlers here to the document.

  2. When you click on the "Fan" button, render_fan is called which pushes a history entry for /fan and renders the FanRoot component, replacing the existing children of #content.

  3. When you click the back button, your browser sets the URL back to / and triggers a popstate event, which Turbolinks catches. Turbolinks restores the original content of the <body> tag, which causes the linked script to be reevaluated, because your JS is loaded in the body of the page instead of the head.

    This causes all of your JS to be rerun, causing a second, separate copy of React to be initialized (and causing your app code to be run again, which is how the Root component gets rerendered). This second copy of React would attach its own event listeners to the document, but React 0.8 has a bug where a second copy of React won't add its own event listeners, so only the first copy of React receives click events but it doesn't know about the onClick handler stored by the second React, so nothing happens.

Newer versions of React don't have this problem -- they're able to keep separate versions encapsulated, so each React instance will keep its own event handlers properly. I tested your app with React 0.10.0 and it works correctly.

However, I believe your use of Turbolinks here is incorrect and may cause you problems in the future. It sounds like to use Turbolinks properly, you should put script elements in your page <head>, but since it looks like you're building a single-page app in React alone, you probably want to disable Turbolinks completely and just bind to popstate yourself in order to re-render when the back button is clicked.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top