Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to Handle Post Request in Isomorphic React + React Router Application

Tags:

I want to build Isomorphic react + react-router application and after a few days googling, now I can achieve isomorphic application that only handles GET request.

Here's what I've done so far:

  1. Server use react-router to handle all request
  2. react-router will call fetchData functions that resides in each React View that matches the route.
  3. Set the data fetched before into props of the React View and render it into string
  4. Inject the string and data fetched before as global variable window.__STATE__ into HTML and deliver the HTML to the client
  5. We have successfully render React App from the server
  6. When the client finished loading our React App javascript, it will try to render. But we pass the state from window.__STATE__ as the props of our React App, and React will not re-render because the state is the same

The problem is it will not work with POST/PUT/DELETE/WHATEVER request. When handling GET request, react-router have information about params and query. For example if we have a route: /user/:uid and client request this url: /user/1?foo=bar, then params would be: {uid: 1} and query would be {foo: 'bar'}

react-router then can pass it down to fetchData function so it will know to fetch user with uid of 1 and do whatever with foo query.

While in POST request, react-router doesn't know about the POST parameters. On Server, of course we could pass the POST parameters to fetchData function, but what about the Client? It doesn't know what the POST parameters are.

Is there a way that the server could tell the Client about the POST parameters? Below is an example of my Login View. I want when user submit the form, the server will render error message on error, or redirect it to dashboard on success.

fetchData.js

import whenKeys from 'when/keys';

export default (authToken, routerState) => {
  var promises = routerState.routes.filter((match) => {
    return match.handler.fetchData;
  }).reduce((promises, match) => {
    promises[match.name] = match.handler.fetchData(authToken, routerState.params, routerState.query);
    return promises;
  }, {});

  return whenKeys.all(promises);
}

server.js

...
app.use((req, res) => {
  const router = Router.create({
    routes,
    location: req.originalUrl,
    onError: next,
    onAbort: (abortReason) => {
      next(abortReason);
    }
  });

  router.run((Handler, state) => {
    fetchData(authToken, state).then((data) => {
      // render matched react View and generate the HTML
      // ...
    })
  });
});
...

login.jsx

import React from 'react';
import DocumentTitle from 'react-document-title';
import api from './api';

export default class Login extends React.Component {

  constructor(props) {
    super(props);

    // how to fill this state with POST parameters on error?
    // how to redirect on success?
    // and remember that this file will be called both from server and client
    this.state = {
      error: '',
      username: '',
      password: ''
    };
  }

  // I saw some people use this function, but it'll only work if
  // the form's method is GET
  static willTransitionTo(transition, params, query) {
     // if only we could read POST parameters here
     // we could do something like this
     transition.wait(
       api.post('/doLogin', postParams).then((data) => {
         transition.redirect(`/dashboard`);
       });
     );
  }

  render() {
    return (
      <DocumentTitle title="Login">
        <div className="alert alert-danger">{this.state.error}</div>
        <form method="post">
          <input type="text" name="username" value={this.state.username}  onChange={this._onFieldChange('username')} placeholder="Username" /><br />
          <input type="password" name="password" value={this.state.password}  onChange={this._onFieldChange('password')} placeholder="Password" /><br />
          <button type="submit">Login</button>
        </form>
      </DocumentTitle>
    );
  }

  _onFieldChange(name) {
    var self = this;
    return (e) => {
      e.preventDefault();
      var nextState = {};
      nextState[name] = e.target.value;
      self.setState(nextState);
    }
  }
}
like image 244
Riko Nagatama Avatar asked Jun 26 '15 17:06

Riko Nagatama


1 Answers

Getting "POST" data on the client

On the client side, you get POST data by extracting values from your form inputs in a way which corresponds to what you would have received on the server had the form been submitted normally.

Using POST data

So now you have your POST data, but you still have the problem that there's no way to feed the POST data into your transition hooks in React Router 0.13.x and earlier. I created a pull request for this feature which has now been closed because it was included as part of the rewrite for the upcoming v1.0 release.

The gist of it is that locations now have a state object for squireling away any extra data you need about the current request/transition (the two are analagous) being handled:

  • On the server, you're dealing with one request at a time, so you create a static Location with data from req.body
  • On the client you pass the state object (containing extracted form data) to transitionTo().

Now your transition hook is capable of receiving the same form data in both environments. If things go well, great! If things don't go well, you need a way to pass errors and re-render the form again. New state object to the rescue again! Use transition.redirect() and pass both input data and errors and you now have everything you need to render on both sides.

I'm not going into more specific detail right now because v1.0 is still in beta and v0.13.x doesn't have the necessary API to do this anyway, but I have a repository which uses the pull request above to implement this workflow with 0.13.x which you could look at in the meantime:

  • isomorphic-lab - the README gives an overview of how things fit together.

Here are some rough flow diagrams of the process, too:

Server POST with errors and redisplay

Server flow diagram

Client POST with errors and redisplay

Client flow diagram


I've also created a few reusable modules related to this scenario:

  • get-form-data gets data from a form's inputs in the format it would have been POSTed in.
  • react-auto-form provides <AutoForm>, which you can use instead of <form> to receive all the data from a form's inputs as an argument to its onSubmit handler
  • react-router-form, which is to <form> what React Router's <Link> is to <a> - it handles triggering a transition to the given action, passing method and body (form data) state - this will be updated for v1.0 soon.
like image 131
Jonny Buchanan Avatar answered Oct 18 '22 10:10

Jonny Buchanan