Skip to content
This repository has been archived by the owner on Jan 22, 2023. It is now read-only.

Question regarding patterns with autofill and server rendering #3

Closed
AndrewIngram opened this issue Jul 18, 2016 · 6 comments
Closed
Labels

Comments

@AndrewIngram
Copy link

As explained pretty well in this article, autofill creates a few problems with server-side rendering, because the change event happens before React finishes initialising. So we end up back on the world of having to use refs and reading the DOM when mounting the form. I'm wondering if you have any preferred patterns for this.

@dvdzkwsk
Copy link
Owner

dvdzkwsk commented Jul 18, 2016

I read through the article but didn't fully understand what the problem was (though from the title I expect I understand it) or what caused it:

React and Redux get initialized on the client side, unaware of the input values of my input fields.

Why is this the case? Shouldn't they have initial state rehydrated from the server (provided by window.__INITIAL_STATE__ [or whatever]). Maybe I'm missing something obvious, as I haven't yet done server-side rendering for forms, so perhaps I can throw together a small demo to see if I can reproduce the issue.

@dvdzkwsk
Copy link
Owner

dvdzkwsk commented Jul 19, 2016

Scratch that last (deleted) comment, I cannot reproduce the issue. If you take a look at this branch: https://github.com/davezuko/react-reformed/tree/server-demo (npm i && npm run server), you'll see that the form gets properly rehydrated. Is there something else that I need to do to properly reproduce the issue?

Server rendering code is here: https://github.com/davezuko/react-reformed/blob/server-demo/demo/server/app.js. The form looks right both loaded as a static page (i.e. no React app to resume on the client) and with the backing client app.

So basically, where I'm at with this is: I don't really have a unique way of approaching this. The form is just rendered straight through props, without any ref usage (which I think is what redux-form uses, which may be the source of their troubles and thus necessitates the workaround, but I could be wrong), so for us it should just work if you are rehydrating the state correctly.

If I'm missing something, please let me know.

@AndrewIngram
Copy link
Author

When i'm talking about autofill, i'm referring to browser behaviour to fill in fields that it recognises with values from it's database, i.e. when Chrome makes the field backgrounds turn yellow.

With client-only rendering, this happens:

  • Page loads
  • Javascript runs
  • React initialises and renders the DOM (including form)
  • Browser recognises the auto-fillable fields and populates them, triggering the change event.
  • Model is correctly updated

With server-side rendering:

  • Initial render happens on the server
  • Page loads
  • Browser recognises the auto-fillable fields and populates them
  • Javascript runs
  • React initialises, but doesn't change the DOM, because the checksums match.
  • No change event is triggered for the form fields, because that happened earlier.
  • The model hasn't been updated.

This is specific example of the general issue with forms and server-side rendering, which is that if you're using a state object to hold form values, it's possible for them to get out of sync with the DOM, because changes can happen before React is initialized and able to handle them.

The solution in the article reads the current values of the fields from the DOM when the form is mounted. It's the only solution i'm aware of for this type of problem, it's just unfortunately because it breaks the purity of never reading the DOM that we get with the client-only solution.

@dvdzkwsk
Copy link
Owner

dvdzkwsk commented Jul 19, 2016

Ok, I understand the problem now, sorry for being so dense. For clarity, what I was missing was that extra step in the sequence of events, since the auto-filling does work. You hint at it in one of the points, but I think it's crucial to point out explicitly:

  • Initial render happens on the server
  • Page loads
  • Browser recognizes the auto-fillable fields and populates them
  • User changes some fields (before [client] React initializes)
  • Javascript runs
  • React initializes, but doesn't change the DOM, because the checksums match.
  • No change event is triggered for the form fields, because that happened earlier.
  • The model hasn't been updated. It's now out of date with what's visible in the form.

I've seen this problem brought up before, in fact the first place I saw it tackled was in an Angular 2 talk (found it, here: https://www.youtube.com/watch?v=0wvZ7gakqV4#t=10m30s). Their approach (called preboot) is to load some additional JS (likely in the head) that will run before the app finishes initializing. The script will listen to and record user events and then play them back to the app behind the scenes once it initializes. This is conceptually very cool but unfortunately probably not a worthwhile investment for smaller apps in React land.

The refs solution is indeed ugly and I'm not a fan of it at all, but for right now I can't think of any more sane of an approach (that doesn't require significantly more work). So, per your initial question: no, I unfortunately don't have an alternate approach. The use of refs seems like a necessary workaround, but now that I understand the problem better I'll play around with it and see what other ideas I can come up with.

@dvdzkwsk
Copy link
Owner

dvdzkwsk commented Jul 19, 2016

So I just did come up with an idea, but I think explicit ref usage is still preferable for clarity.

const rehydrateFromDOM = (WrappedComponent) => {
  class RehydratableFromDOM extends React.Component {
    componentDidMount () {
      const form = ReactDOM.findDOMNode(this._form)

      // this currently only looks for inputs, and is pretty specific, so it would
      // have to be expanded to fit your needs.
      const model = [...form.querySelectorAll('input')]
        .reduce((acc, input) => {
          switch (input.type) {
            case 'checkbox':
              if (input.checked) {
                acc[input.name] = (acc[input.name] || []).concat(input.value)
              }
              break
            default:
              acc[input.name] = input.value
          }
          return acc
        }, {})
      this.props.setModel(model)
    }

    _onRef = (el) => {
      this._form = el
    }

    render () {
      return React.createElement(WrappedComponent, {
        ...this.props,
        ref: this._onRef,
      })
    }
  }
  return RehydratableFromDOM
}

Then you can use it as follows:

const createFormContainer = compose(
  reformed(),
  rehydrateFromDOM,
)

createFormContainer(YourForm)

I delayed rendering, messed with the inputs, and then initialized the client application manually. It correctly rehydrated the model from what existed in the DOM, so it at least works on a prototype level. The one big gotcha with this, of course, is that none of the higher order components can be stateless since they cannot have refs attached to them. I guess you could render the wrapped component in a div and just run the querySelector off of that, but it seems wrong to insert a <div> unexpectedly.

@tstirrat15
Copy link

tstirrat15 commented May 4, 2017

I found this issue when I was looking into the same problem, and I found the preboot library. I've yet to implement it, but it seems like it may be a good way around these problems.

It works by being the first thing to load on the page, and then listening for input events, which can then be played back after the client code has rendered/bootstrapped.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

3 participants