Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initialize a new HTTP client for each web request #1237

Merged
merged 6 commits into from
Apr 25, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion LICENSE.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
The MIT License

Copyright (c) 2014-present Konstantin Tarkus, KriaSoft LLC.
Copyright (c) 2014-present Konstantin Tarkus, Kriasoft LLC.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
78 changes: 53 additions & 25 deletions docs/data-fetching.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,60 @@
## Data Fetching with WHATWG Fetch

There is isomorphic `core/fetch` module that can be used the same way in both
client-side and server-side code as follows:

```jsx
import fetch from '../core/fetch';

export const path = '/products';
export const action = async () => {
const response = await fetch('/graphql?query={products{id,name}}');
const data = await response.json();
return <Layout><Products {...data} /></Layout>;
};
## Data Fetching

At a bare minimum you may want to use [HTML5 Fetch API][fetch] as an HTTP client utility for
making Ajax request to the [data API server][nodeapi]. This API is supported natively in all the
major browsers except for IE (note, that Edge browser does support Fetch).

**React Starter Kit** is pre-configured with [`whatwg-fetch`][wfetch] polyfill for the browser
environment and [`node-fetch`][nfetch] module for the server-side environment (see
[`src/createFetch.js`](../src/createFetch.js)), allowing you to use the `fetch(url, options)`
method universally in both the client-side and server-side code bases.

In order to avoid the the amount of boilerplate code needed when using the raw `fetch(..)`
function, a simple wrapper was created that provides a base URL of the data API server, credentials
(cookies), CORS etc. For example, in a browser environment the base URL of the data API server
might be an empty string, so when you make an Ajax request to the `/graphql` endpoint it's being
sent to the same origin, and when the same code is executed on the server, during server-side
rendering, it fetches data from the `http://api:8080/graphql` endpoint (`node-fetch` doesn't
support relative URLs for obvious reasons).

Because of these subtle differences of how the `fetch` method works internally, it makes total
sense to pass it as a `context` variable to your React application, so it can be used from either
routing level or from inside your React components as follows:

#### Route Example

```js
{
path: '/posts/:id',
async action({ params, fetch }) {
const resp = await fetch(`/api/posts/${params.id}`, { method: 'GET' });
const data = await resp.json();
return { title: data.title, component: <Post {...data} /> };
}
}
```

When this code executes on the client, the Ajax request will be sent via
GitHub's [fetch](https://github.com/github/fetch) library (`whatwg-fetch`),
that itself uses XHMLHttpRequest behind the scene unless `fetch` is supported
natively by the user's browser.
#### React Component

```js
class Post extends React.Component {
static context = { fetch: PropTypes.func.isRequired };
handleDelete = (event) => {
event.preventDefault();
const id = event.target.dataset['id'];
this.context.fetch(`/api/posts/${id}`, { method: 'DELETE' }).then(...);
};
render() { ... }
}
```

Whenever the same code executes on the server, it uses
[node-fetch](https://github.com/bitinn/node-fetch) module behind the scene that
itself sends an HTTP request via Node.js `http` module. It also converts
relative URLs to absolute (see `./core/fetch/fetch.server.js`).
#### Related articles

Both `whatwg-fetch` and `node-fetch` modules have almost identical API. If
you're new to this API, the following article may give you a good introduction:
* [That's so fetch!](https://jakearchibald.com/2015/thats-so-fetch/) by [Jake Archibald](https://twitter.com/jaffathecake)

https://jakearchibald.com/2015/thats-so-fetch/

[fetch]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
[wfetch]: https://github.com/github/fetchno
[nfetch]: https://github.com/bitinn/node-fetch
[nodeapi]: https://github.com/kriasoft/nodejs-api-starter

4 changes: 2 additions & 2 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ Before you start, take a moment to see how the project structure looks like:
├── /public/ # Static files which are copied into the /build/public folder
├── /src/ # The source code of the application
│ ├── /components/ # React components
│ ├── /core/ # Core framework and utility functions
│ ├── /data/ # GraphQL server schema and data models
│ ├── /routes/ # Page/screen components along with the routing information
│ ├── /client.js # Client-side startup script
│ ├── /config.js # Global application settings
│ └── /server.js # Server-side startup script
│ ├── /server.js # Server-side startup script
│ └── ... # Other core framework modules
├── /test/ # Unit and end-to-end tests
├── /tools/ # Build automation scripts and utilities
│ ├── /lib/ # Library for utility snippets
Expand Down
24 changes: 12 additions & 12 deletions docs/recipes/how-to-implement-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Let's see how a custom routing solution under 100 lines of code may look like.

First, you will need to implement the **list of application routes** in which each route can be
represented as an object with properties of `path` (a parametrized URL path string), `action`
(a function), and optionally `children` (a list of sub-routes, each of which is a route object).
(a function), and optionally `children` (a list of sub-routes, each of which is a route object).
The `action` function returns anything - a string, a React component, etc. For example:

#### `src/routes/index.js`
Expand Down Expand Up @@ -42,7 +42,7 @@ return `{ id: '123' }` while calling `matchURI('/tasks/:id', '/foo')` must retur
Fortunately, there is a great library called [`path-to-regexp`](https://github.com/pillarjs/path-to-regexp)
that makes this task very easy. Here is how a URL matcher function may look like:

#### `src/core/router.js`
#### `src/router.js`

```js
import toRegExp from 'path-to-regexp';
Expand All @@ -67,7 +67,7 @@ action method returns anything other than `null` or `undefined` return that to t
Otherwise, it should continue iterating over the remaining routes. If none of the routes match to the
provided URL string, it should throw an exception (Not found). Here is how this function may look like:

#### `src/core/router.js`
#### `src/router.js`

```js
import toRegExp from 'path-to-regexp';
Expand All @@ -93,12 +93,12 @@ export default { resolve };
That's it! Here is a usage example:

```js
import router from './core/router';
import router from './router';
import routes from './routes';

router.resolve(routes, { pathname: '/tasks' }).then(result => {
console.log(result);
// => { title: 'To-do', component: <TodoList .../> }
// => { title: 'To-do', component: <TodoList .../> }
});
```

Expand All @@ -108,10 +108,10 @@ npm module to handles this task for you. It is the same library used in React Ro
wrapper over [HTML5 History API](https://developer.mozilla.org/docs/Web/API/History_API) that
handles all the tricky browser compatibility issues related to client-side navigation.

First, create `src/core/history.js` file that will initialize a new instance of the `history` module
First, create `src/history.js` file that will initialize a new instance of the `history` module
and export is as a singleton:

#### `src/core/history.js`
#### `src/history.js`

```js
import createHistory from 'history/lib/createBrowserHistory';
Expand All @@ -125,8 +125,8 @@ Then plug it in, in your client-side bootstrap code as follows:

```js
import ReactDOM from 'react-dom';
import history from './core/history';
import router from './core/router';
import history from './history';
import router from './router';
import routes from './routes';

const container = document.getElementById('root');
Expand Down Expand Up @@ -157,7 +157,7 @@ In order to trigger client-side navigation without causing full-page refresh, yo

```js
import React from 'react';
import history from '../core/history';
import history from '../history';

class App extends React.Component {
transition = event => {
Expand All @@ -181,9 +181,9 @@ class App extends React.Component {

Though, it is a common practice to extract that transitioning functionality into a stand-alone
(`Link`) component that can be used as follows:

```html
<Link to="/tasks/123">View Task #123</Link>
<Link to="/tasks/123">View Task #123</Link>
```

### Routing in React Starter Kit
Expand Down
44 changes: 22 additions & 22 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,36 +21,36 @@
"core-js": "^2.4.1",
"express": "^4.15.2",
"express-graphql": "^0.6.4",
"express-jwt": "^5.1.0",
"express-jwt": "^5.3.0",
"fastclick": "^1.0.6",
"graphql": "^0.9.2",
"graphql": "^0.9.3",
"history": "^4.6.1",
"isomorphic-style-loader": "^1.1.0",
"isomorphic-fetch": "^2.2.1",
"isomorphic-style-loader": "^2.0.0",
"jsonwebtoken": "^7.3.0",
"node-fetch": "^1.6.3",
"normalize.css": "^6.0.0",
"passport": "^0.3.2",
"passport-facebook": "^2.1.1",
"pretty-error": "^2.1.0",
"prop-types": "^15.5.6",
"query-string": "^4.3.2",
"react": "^15.5.3",
"react-dom": "^15.5.3",
"prop-types": "^15.5.8",
"query-string": "^4.3.4",
"react": "^15.5.4",
"react-dom": "^15.5.4",
"sequelize": "^3.30.4",
"serialize-javascript": "^1.3.0",
"source-map-support": "^0.4.14",
"sqlite3": "^3.1.8",
"universal-router": "^3.0.0",
"whatwg-fetch": "^2.0.3"
"universal-router": "^3.1.0"
},
"devDependencies": {
"assets-webpack-plugin": "^3.5.1",
"autoprefixer": "^6.7.7",
"babel-cli": "^6.24.1",
"babel-core": "^6.24.1",
"babel-eslint": "^7.2.1",
"babel-loader": "^6.4.1",
"babel-eslint": "^7.2.3",
"babel-loader": "^7.0.0",
"babel-plugin-rewire": "^1.1.0",
"babel-preset-env": "^1.3.3",
"babel-preset-env": "^1.4.0",
"babel-preset-react": "^6.24.1",
"babel-preset-react-optimize": "^1.0.1",
"babel-preset-stage-2": "^6.24.1",
Expand All @@ -62,7 +62,7 @@
"chokidar": "^1.6.1",
"css-loader": "^0.28.0",
"editorconfig-tools": "^0.1.1",
"enzyme": "^2.8.0",
"enzyme": "^2.8.2",
"eslint": "^3.19.0",
"eslint-config-airbnb": "^14.1.0",
"eslint-loader": "^1.7.1",
Expand All @@ -77,21 +77,21 @@
"lint-staged": "^3.4.0",
"markdown-it": "^8.3.1",
"mkdirp": "^0.5.1",
"mocha": "^3.2.0",
"mocha": "^3.3.0",
"pixrem": "^3.0.2",
"pleeease-filters": "^3.0.1",
"postcss": "^5.2.16",
"postcss": "^5.2.17",
"postcss-calc": "^5.3.1",
"postcss-color-function": "^3.0.0",
"postcss-custom-media": "^5.0.1",
"postcss-custom-properties": "^5.0.2",
"postcss-custom-selectors": "^3.0.0",
"postcss-flexbugs-fixes": "^2.1.0",
"postcss-flexbugs-fixes": "^2.1.1",
"postcss-global-import": "^1.0.0",
"postcss-import": "^9.1.0",
"postcss-loader": "^1.3.3",
"postcss-media-minmax": "^2.1.2",
"postcss-nested": "^1.0.0",
"postcss-nested": "^1.0.1",
"postcss-nesting": "^2.3.1",
"postcss-pseudoelements": "^4.0.0",
"postcss-selector-matches": "^2.0.5",
Expand All @@ -108,11 +108,11 @@
"stylelint": "^7.10.1",
"stylelint-config-standard": "^16.0.0",
"url-loader": "^0.5.8",
"webpack": "^2.3.3",
"webpack-bundle-analyzer": "^2.3.1",
"webpack-dev-middleware": "^1.10.1",
"webpack": "^2.4.1",
"webpack-bundle-analyzer": "^2.4.0",
"webpack-dev-middleware": "^1.10.2",
"webpack-hot-middleware": "^2.18.0",
"write-file-webpack-plugin": "^4.0.0"
"write-file-webpack-plugin": "^4.0.2"
},
"babel": {
"presets": [
Expand Down
File renamed without changes.
18 changes: 12 additions & 6 deletions src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ import ReactDOM from 'react-dom';
import FastClick from 'fastclick';
import queryString from 'query-string';
import { createPath } from 'history/PathUtils';
import history from './core/history';
import App from './components/App';
import { updateMeta } from './core/DOMUtils';
import { ErrorReporter, deepForceUpdate } from './core/devUtils';
import createFetch from './createFetch';
import history from './history';
import { updateMeta } from './DOMUtils';
import { ErrorReporter, deepForceUpdate } from './devUtils';

/* eslint-disable global-require */

Expand All @@ -29,6 +30,10 @@ const context = {
const removeCss = styles.map(x => x._insertCss());
return () => { removeCss.forEach(f => f()); };
},
// Universal HTTP client
fetch: createFetch({
baseUrl: window.App.apiUrl,
}),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add credentials: 'same-origin' here?

};

// Switch off the native scroll restoration behavior and handle it manually
Expand Down Expand Up @@ -87,7 +92,7 @@ FastClick.attach(document.body);
const container = document.getElementById('app');
let appInstance;
let currentLocation = history.location;
let router = require('./core/router').default;
let router = require('./router').default;

// Re-render the app when window.location changes
async function onLocationChange(location, action) {
Expand All @@ -109,6 +114,7 @@ async function onLocationChange(location, action) {
const route = await router.resolve({
path: location.pathname,
query: queryString.parse(location.search),
fetch: context.fetch,
});

// Prevent multiple page renders during the routing process
Expand Down Expand Up @@ -161,8 +167,8 @@ if (__DEV__) {

// Enable Hot Module Replacement (HMR)
if (module.hot) {
module.hot.accept('./core/router', () => {
router = require('./core/router').default;
module.hot.accept('./router', () => {
router = require('./router').default;

if (appInstance) {
try {
Expand Down
2 changes: 2 additions & 0 deletions src/components/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const ContextType = {
// Enables critical path CSS rendering
// https://github.com/kriasoft/isomorphic-style-loader
insertCss: PropTypes.func.isRequired,
// Universal HTTP client
fetch: PropTypes.func.isRequired,
};

/**
Expand Down
Loading