Skip to content
This repository has been archived by the owner on Dec 30, 2022. It is now read-only.

Commit

Permalink
feat(server-side rendering): Add API features for server-side rendering
Browse files Browse the repository at this point in the history
+ guide
+ examples
  • Loading branch information
mthuret authored and vvo committed Jul 7, 2017
1 parent 481320e commit 86b14d1
Show file tree
Hide file tree
Showing 43 changed files with 1,246 additions and 23 deletions.
35 changes: 29 additions & 6 deletions .babelrc
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,33 @@
// used for storybooks and regular build
"development": {
"presets": [
["env", { "targets": { "browsers": ["last 2 versions", "ie >= 9"] } }],
["env", {
"targets": {
"browsers": ["last 2 versions", "ie >= 9"]
}
}],
"react",
"stage-2"
],
"plugins": [
["transform-runtime", {
"helpers": false,
"polyfill": false,
"regenerator": true
}],
"transform-react-jsx-source",
"react-hot-loader/babel"
"react-hot-loader/babel",
"transform-regenerator",
]
},
// used for (require();) builds and node
"commonjs": {
"presets": [
["env", { "targets": { "browsers": ["last 2 versions", "ie >= 9"] } }],
["env", {
"targets": {
"browsers": ["last 2 versions", "ie >= 9"]
}
}],
"react",
"stage-2",
"babili"
Expand All @@ -25,19 +39,28 @@
// jest sets node env so we can't use `development`
"test": {
"presets": [
["env", { "targets": { "browsers": ["last 2 versions", "ie >= 9"] } }],
["env", {
"targets": {
"browsers": ["last 2 versions", "ie >= 9"]
}
}],
"react",
"stage-2"
]
},
// used for es2015 modules build (import; export;)
"es": {
"presets": [
["env", { "targets": { "browsers": ["last 2 versions", "ie >= 9"] }, "modules": false }],
["env", {
"targets": {
"browsers": ["last 2 versions", "ie >= 9"]
},
"modules": false
}],
"react",
"stage-2",
"babili"
]
}
}
}
}
2 changes: 2 additions & 0 deletions docgen/layouts/common/header.pug
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ nav.cm-navigation
a(href="widgets/", role='treeitem', data-link = 'true') Widgets
li
a(href="connectors/", role='treeitem', data-link = 'true') Connectors
li
a(href="server-side-rendering/", role='treeitem', data-link = 'true') Server-side Rendering
li.cm-menu__list__item(class=h.maybeActive(navPath, "examples/"))
a(aria-controls='dropdown-4', aria-expanded='false', data-toggle-dropdown='dropdown-4', class="dropdown-toggler") Examples <span class="glyph">&#x25BC;</span>
ul#dropdown-4.simple-dropdown(role='tree', tabindex='-1')
Expand Down
66 changes: 66 additions & 0 deletions docgen/layouts/server-side-rendering.pug
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
extends archetypes/content-with-menu.pug

include mixins/nav.pug

block navigation
+nav(navPath, navigation, mainTitle || title, withHeadings && headings || [])

block content
h2#description Description
a.anchor(href=`${navPath}#description`)
div!=h.markdown(description)
h2#proptypes Params
a.anchor(href=`${navPath}#proptypes`)
if params
table.api.params
tbody
each type in params
tr.api-entry-values
td.api-entry-name
div.api-entry(id=`default-props-entry-${name}-${type.name}`)
=`${type.name}${type.isRequired ? '*' : ''}`
a.anchor(href=`${navPath}#default-props-entry-${name}-${type.name}`)
td.api-entry-type
span type:
= ' '
code=type.type.names
td.api-entry-default-value
if type.defaultValue
span default:
= ' '
code=type.defaultValue
else
span &nbsp;
tr.api-entry-description
td(colspan=3)!=h.markdown(type.description)
else
p This function has no params.
h2#proptypes Returns
a.anchor(href=`${navPath}#proptypes`)
if returns
table.api.returns
tbody
each type in returns
tr.api-entry-values
td.api-entry-type
span type:
= ' '
code=type.type.names
tr.api-entry-description
td(colspan=3)!=h.markdown(type.description)
else
p This function returns nothing.
if requirements
h2#requirements Requirements
a.anchor(href=`${navPath}#requirements`)
div!=h.markdown(requirements)
if operations
h2#operations Operations
a.anchor(href=`${navPath}#operations`)
div!=h.markdown(operations)
if examples
h2#example Example usage
a.anchor(href=`${navPath}#example`)
each example in examples
p!=h.highlight(example, {lang: 'jsx'})

1 change: 1 addition & 0 deletions docgen/middlewares.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const common = [
reactPackage('src/connectors/*.js'),
reactPackage('src/core/InstantSearch.js'),
reactPackage('src/core/Index.js'),
reactPackage('src/core/findResultsState.js'),
],
{
ignore: '**/*.test.js',
Expand Down
9 changes: 5 additions & 4 deletions docgen/plugins/jsdoc-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ export default function() {
o.kind &&
(o.kind === 'component' ||
o.kind === 'widget' ||
o.kind === 'connector')
o.kind === 'connector' ||
o.kind === 'server-side-rendering')
),
'kind'
);
Expand All @@ -80,9 +81,9 @@ export default function() {
stats: fileFromMetalsmith && fileFromMetalsmith.stats,
filename: fileFromMetalsmith && fileFromMetalsmith.filename,
title,
mainTitle: `${data.kind.charAt(0).toUpperCase()}${data.kind.slice(
1
)}s`, //
mainTitle: data.kind === 'server-side-rendering'
? 'Server-side Rendering'
: `${data.kind.charAt(0).toUpperCase()}${data.kind.slice(1)}s`, //
withHeadings: false,
layout: `${data.kind}.pug`,
category: data.kind,
Expand Down
6 changes: 6 additions & 0 deletions docgen/src/examples/Recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ examples: [{
}, {
id: 'geo-search',
title: 'Geo search using dynamic search parameters'
}, {
id: 'server-side-rendering',
title: 'Server Side Rendering'
}, {
id: 'next-app',
title: 'Server Side Rendering using next.js'
}]
examplesEndpoint: https://github.com/algolia/react-instantsearch/tree/master/packages/react-instantsearch/examples
---
Expand Down
137 changes: 134 additions & 3 deletions docgen/src/guide/Server-side_rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,139 @@ category: guide
navWeight: 30
---

React InstantSearch is not yet compatible with server-side rendering. This is one of our priority topics
right now. We will soon provide API entries to handle this usecase.
React InstantSearch is compatible with server-side rendering. We provide an API that can be used
with any server-side rendering solution. Such as [Next.js](https://github.com/zeit/next.js/).

## Examples

* Using [Express and ReactDOMServer](https://github.com/algolia/react-instantsearch/tree/master/packages/react-instantsearch/examples/server-side-rendering).
* Using [Next.js](https://github.com/algolia/react-instantsearch/tree/master/packages/react-instantsearch/examples/next-app).

If you are looking for an example using another solution, read how to do custom implementations.

## Three steps custom implementation

We provide a new API entry, `createIntantSearch`, available under `'react-instantsearch/server'`.

When called, `createIntantSearch` returns:

* A dedicated [`<InstantSearch>`](widgets/InstantSearch.html) component accepting a `resultsState` prop containing the Algolia results.
* A `findResultsState` function to retrieve a `resultsState`.

The server-side reference is available in the [API docs](http://localhost:3000/server-side-renderings/createInstantSearch.html).

We split this guide in three parts:
- App.js is the server and browser shared main React component from your application
- server.js is a simple Node.js http server, it's the main server entry
- browser.js is the main browser entry that ultimately gets compiled to bundle.js

### App.js

`App.js` is usually the main entry point of your React application, it exports an <App> component.

```jsx
import React, { Component } from 'react';
import { SearchBox, Hits } from 'react-instantsearch/dom';
import { createInstantSearch } from 'react-instantsearch/server';

// Now we create a dedicated `InstantSearch` component
const { InstantSearch, findResultsState } = createInstantSearch();

class App extends Component {
render() {
return (
<InstantSearch
appId="appId"
apiKey="apiKey"
indexName="indexName"
searchState={this.props.searchState || {}}
resultsState={this.props.resultsState || {}}
>
<SearchBox />
<Hits />
</InstantSearch>
);
}
}

export { App, findResultsState };
```

**Steps:**
- Use `createInstantSearch()` to get a `findResultsState` function and a dedicated `<InstantSearch>` component (instead of importing the one under `react-instantsearch/dom`)
- Export `<App>` (to be used by browser and server code) and `findResultsState` (to be used by server code)

**Notes:**
* **Keep a reference** to your dedicated `InstantSearch` component, do not re-create it at each render loop of your `App` component (This will fail: `... render() { return createInstantSearch(); }`)
* If you want to use multiple `<InstantSearch>` components then you need to create dedicated `<InstantSearch>` components for each of them.
* If you are syncing the searchState to the url for [proper routing](guide/Routing.html), pass a [`searchState`](guide/Search_state.html) to the `InstantSearch` component.

### server.js

```jsx
import React from 'react';
import { createServer } from 'http';
import { App, findResultsState } from './app.js';
import { renderToString } from 'react-dom/server';

const server = createServer((req, res) => {
const searchState = {searchState: {query: 'chair'}};
const resultsState = await findResultsState(App);
const appInitialState = {searchState, resultsState}
const appAsString = renderToString(<App {...appInitialState} />);
res.send(
`
<!doctype html>
<html>
<body>
<h1>Awesome server-side rendered search</h1>
<did id="root">${appAsString}</div>
<script>window.__APP_INITIAL_STATE__ = ${JSON.stringify(appInitialState)}</script>
<script src="bundle.js"></script> <!-- this is the build of browser.js -->
</body>
</html>`
);
});

server.listen(8080);
```

**Notes:**
- You have to transpile (with [Babel](https://babeljs.io/) for example) your server-side code to be able to use JSX and import statements.
- `__APP_INITIAL_STATE__` will be used so that React ensures what was sent by the server matches what the browser expects (checksum)

### browser.js

This is the last part that does the plumbing between server-side rendering and the
start of the application on the browser.

```jsx
import React from 'react';
import { render } from 'react-dom';
import { App } from './index';

render(
<App {...window.__APP_INITIAL_STATE__} />,
document.querySelector('#root')
);
```

**Notes:**
- A request will still be sent to Algolia when React mount your `<App>` in the browser.

👌 **That's it!** You know the basics of doing a custom server-side implementation.

## The InstantSearch = createInstantSearch() pattern

It might be a bit confusing why we cannot use directly the `<InstantSearch>` component you were previously using, this part details a bit why we took this approach.

React InstantSearch is a declarative API that programmatically builds an Algolia query. Based on every widget used, and their own options, we compute a set of parameters that should be sent to Algolia.

While doing browser rendering, we need to first render a Component tree alone (to compute every parameter) to then re-render it again with results. Unfortunately React server-side rendering feature does not allows for such a pattern.

The only solution is to provide an API to get a set of results to then be passed down to an `<InstantSearch>` component as a prop.

As this can be confusing, you might have better ideas on how we could have implemented this, if so, reach out on our GitHub or on our [discourse forum](https://discourse.algolia.com/).

<div class="guide-nav">
<div class="guide-nav-left">
Expand All @@ -16,4 +147,4 @@ right now. We will soon provide API entries to handle this usecase.
<div class="guide-nav-right">
Next: <a href="guide//Autocomplete_menu.html">Autocomplete menu →</a>
</div>
</div>
</div>
Binary file added docgen/src/images/examples/next-app.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions docgen/src/server-side-rendering/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
title: Server Side Rendering
layout: main.pug
category: server-side-rendering
showInNav: false
---


When using server side rendering, instead of having your browser download a minimal HTML page that JavaScript will fill, the initial content is generated on the server. Then, the browser will download a page with the HTML content already in place.

Usually server side rendering is considered to improve SEO and performances.

The [Server-side rendering guide](guide/Server-side_rendering.html) explains in details how you can use Server Side Rendering with React InstantSearch.
19 changes: 19 additions & 0 deletions packages/react-instantsearch/examples/next-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.

# dependencies
node_modules

# testing
/coverage

# production
/build
/dist
/.next

# misc
.DS_Store
.env
npm-debug.log*
yarn-debug.log*
yarn-error.log*
10 changes: 10 additions & 0 deletions packages/react-instantsearch/examples/next-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
This example shows how to do server side rendering with next.js and React InstantSearch

To start the example:

```sh
yarn install --no-lockfile
yarn run dev
```

Read more about `react-instantsearch` [in our documentation](https://community.algolia.com/react-instantsearch/).

0 comments on commit 86b14d1

Please sign in to comment.