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

Commit

Permalink
Integrated SSR (#83)
Browse files Browse the repository at this point in the history
create initial system for SSR and server side hydration
  • Loading branch information
James Baxley authored Jul 12, 2016
1 parent dbb03dd commit 2847115
Show file tree
Hide file tree
Showing 8 changed files with 592 additions and 39 deletions.
50 changes: 28 additions & 22 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,75 +2,81 @@

Expect active development and potentially significant breaking changes in the `0.x` track. We'll try to be diligent about releasing a `1.0` version in a timely fashion (ideally within 1 or 2 months), so that we can take advantage of SemVer to signify breaking changes from that point on.

### v0.3.16

- Feature: integrated SSR [#83](https://github.com/apollostack/react-apollo/pull/83)
- Feature: added ability to hoist statics on components [#99](https://github.com/apollostack/react-apollo/pull/99)
- Bug: Don't strip data away from the component when the query errors [#98](https://github.com/apollostack/react-apollo/pull/98)

### v0.3.15

Bug: Fixed issue where react native would error on aggressive cloneing of client
- Bug: Fixed issue where react native would error on aggressive cloneing of client

### v0.3.14

Feature: pass through all methods on apollo client
- Feature: pass through all methods on apollo client

### v0.3.13

Bug: fixed issue causing errors to be passed to apollo-client [#89](https://github.com/apollostack/react-apollo/pull/89)
- Bug: fixed issue causing errors to be passed to apollo-client [#89](https://github.com/apollostack/react-apollo/pull/89)

### v0.3.11/12

Bug: fixed overrendering of components on redux state changes
- Bug: fixed overrendering of components on redux state changes

### v0.3.10

Bug: fixed bug where SSR would fail due to later updates. This should also prevent unmounted components from throwing errors.
- Bug: fixed bug where SSR would fail due to later updates. This should also prevent unmounted components from throwing errors.

### v0.3.9

Feature: provide add `watchQuery` to components via `connect`
- Feature: provide add `watchQuery` to components via `connect`

### v.0.3.8

Bug: Don't use old props on store change change
- Bug: Don't use old props on store change change

### v.0.3.7

Bug: Reset loading state when a refetched query has returned
- Bug: Reset loading state when a refetched query has returned

### v0.3.6

Bug: Loading state is no longer true on uncalled mutations.
Improvement: don't set the loading state to false if forceFetch is true
- Bug: Loading state is no longer true on uncalled mutations.
- Improvement: don't set the loading state to false if forceFetch is true

### v0.3.5

Return promise from the refetch method

### v0.3.4

Bug: Fix bug where state / props weren't accurate when executing mutations.
Perf: Increase performance by limiting re-renders and re-execution of queries.
- Bug: Fix bug where state / props weren't accurate when executing mutations.
- - Improvement: Increase performance by limiting re-renders and re-execution of queries.
Chore: Split tests to make them easier to maintain.

### v0.3.2 || v0.3.3 (publish fix)

Feature: add `startPolling` and `stopPolling` to the prop object for queries
Bug: Fix bug where full options were not being passed to watchQuery
- Feature: add `startPolling` and `stopPolling` to the prop object for queries
- Bug: Fix bug where full options were not being passed to watchQuery

### v0.3.1

Support 0.3.0 of apollo-client
- Feature: Support 0.3.0 of apollo-client

### v0.3.0

Change Provider export to be ApolloProvider and use Provider from react-redux
- Feature: Change Provider export to be ApolloProvider and use Provider from react-redux

### v0.2.1

Support 0.1.0 and 0.2.0 of apollo-client
- Feature: Support 0.1.0 and 0.2.0 of apollo-client

### v0.2.0

**Breaking change:**

Remove `result` key in favor of dynamic key matching root fields of the query or mutation. (https://github.com/apollostack/react-apollo/pull/31)
- Feature: Remove `result` key in favor of dynamic key matching root fields of the query or mutation. (https://github.com/apollostack/react-apollo/pull/31)

```js
{
Expand All @@ -92,19 +98,19 @@ becomes

### v0.1.5

Get state directly from redux store internally
- Bug: Get state directly from redux store internally

### v0.1.4

Fix bug with willReceiveProps
- Bug: Fix bug with willReceiveProps

### v0.1.2

Adjust loading lifecycle marker to better match the behavior of apollo-client (https://github.com/apollostack/react-apollo/pull/11)
Bug: - Adjust loading lifecycle marker to better match the behavior of apollo-client [#11](https://github.com/apollostack/react-apollo/pull/11)

### v0.1.1

Update to support new observable API from apollo-client (https://github.com/apollostack/react-apollo/pull/9)
Feature: - Update to support new observable API from apollo-client [#9](https://github.com/apollostack/react-apollo/pull/9)

### v0.1.0

Expand Down
6 changes: 6 additions & 0 deletions global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ declare module 'lodash.isequal' {
export = main.isEqual;
}


declare module 'hoist-non-react-statics' {
interface Component {
new(...args:any[]);
Expand All @@ -26,3 +27,8 @@ declare module 'hoist-non-react-statics' {
namespace hoistNonReactStatics {}
export = hoistNonReactStatics;
}

declare module 'lodash.flatten' {
import main = require('~lodash/index');
export = main.flatten;
}
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
{
"name": "react-apollo",
"version": "0.3.15",
"version": "0.3.16",
"description": "React data container for Apollo Client",
"main": "index.js",
"scripts": {
"pretest": "npm run compile",
"test": "mocha --require ./test/fixtures/setup.js --reporter spec --full-trace --recursive ./lib/test",
"posttest": "npm run lint",
"filesize": "npm run compile:browser && ./scripts/filesize.js --file=./dist/index.min.js --maxGzip=13",
"filesize": "npm run compile:browser && ./scripts/filesize.js --file=./dist/index.min.js --maxGzip=15",
"compile": "tsc",
"compile:browser": "rm -rf ./dist && mkdir ./dist && browserify ./lib/src/index.js --i react --i apollo-client -o=./dist/index.js && npm run minify:browser",
"minify:browser": "uglifyjs --compress --mangle --screw-ie8 -o=./dist/index.min.js -- ./dist/index.js",
"watch": "tsc -w",
"lint": "tslint src/*.ts* && tslint test/*.ts*",
"lint": "tslint 'src/*.ts*' && tslint 'test/*.ts*'",
"coverage": "istanbul cover ./node_modules/mocha/bin/_mocha -- --require ./test/fixtures/setup.js --reporter spec --full-trace --recursive ./lib/test",
"postcoverage": "remap-istanbul --input coverage/coverage.json --type lcovonly --output coverage/lcov.info"
},
Expand Down Expand Up @@ -70,6 +70,7 @@
"dependencies": {
"hoist-non-react-statics": "^1.2.0",
"invariant": "^2.2.1",
"lodash.flatten": "^4.2.0",
"lodash.isequal": "^4.1.1",
"lodash.isobject": "^3.0.2",
"object-assign": "^4.0.1",
Expand Down
8 changes: 7 additions & 1 deletion src/connect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ export default function connect(opts?: ConnectOptions) {

let { mapQueriesToProps, mapMutationsToProps } = opts;

let mapQueries;
if (mapQueriesToProps) {
mapQueries = true;
}

// clean up the options for passing to redux
delete opts.mapQueriesToProps;
delete opts.mapMutationsToProps;
Expand Down Expand Up @@ -103,7 +108,6 @@ export default function connect(opts?: ConnectOptions) {

// Helps track hot reloading.
const version = nextVersion++;

return function wrapWithApolloComponent(WrappedComponent) {
// react-redux will wrap this further with Connect(...).
const apolloConnectDisplayName = `Apollo(${getDisplayName(WrappedComponent)})`;
Expand All @@ -115,6 +119,8 @@ export default function connect(opts?: ConnectOptions) {
store: PropTypes.object.isRequired,
client: PropTypes.object.isRequired,
};
// for use with getData during SSR
static mapQueriesToProps = mapQueries ? mapQueriesToProps : false;

// react / redux and react dev tools (HMR) needs
public state: any; // redux state
Expand Down
195 changes: 195 additions & 0 deletions src/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@

import { Children, createElement } from 'react';
import * as ReactDOM from 'react-dom/server';
import ApolloClient from 'apollo-client';
import flatten = require('lodash.flatten');
import assign = require('object-assign');

/*
React components can return a `falsy` (null, false) value,
representation of a native DOM component (such as <div /> or React.DOM.div())
or another composite component. Components can have a render function (for components).
They can also pass through children which we want to analyze as well.
To get data from `connect()` components we do a few things:
1. if ssr is not falsy, move the query to a place to batch call it
Ideally, we go through the tree and find all `connect()`s (recursively going through tree)
If we reach the end of all nodes, we kick off the queries. Once queries have returned,
we try to go through their children components again to see if we discover any
more queries. Then once we reach th end, we render the dom.
We recursively do this until the tree is done.
So! Given a component:
1. See if it is falsy (end of line)
2. Bulid the context and props (global props + parent props)
3. See if the component is a `connect()`
3a. Get the queries using props + state
3b. as long as ssr != false, pass the query to the array to be called
4. Create the component (or child if connect) (`componentWillMount` will run)
5. Render the component
6. Repeat
*/

declare interface Context {
client?: ApolloClient;
store?: any;
[key: string]: any;
}

declare interface QueryTreeArgument {
component: any;
queries?: any[];
context?: Context;
}

export function getPropsFromChild(child) {
const { props, type } = child;
let ownProps = assign({}, props);
if (type && type.defaultProps) ownProps = assign(type.defaultProps, props);
return ownProps;
}

export function getChildFromComponent(component) {
// See if this is a class, or stateless function
if (component && component.render) return component.render();
return component;
}

export function processQueries(queries, client): Promise<any> {
queries = flatten(queries)
.map((queryDetails: any) => {
const { query, component, ownProps, key, context } = queryDetails;
return client.query(query)
.then(result => {
const { data, errors } = result as any;
ownProps[key] = assign({ loading: false, errors }, data);
return { component, ownProps: assign({}, ownProps), context: assign({}, context) };
});
});

return Promise.all(queries);
}

const defaultReactProps = { loading: true, errors: null };
function getQueriesFromTree({ component, context = {}, queries = []}: QueryTreeArgument) {

if (!component) return;
let { client, store } = context;

// stateless function
if (typeof component === 'function') component = { type: component };
const { type, props } = component;

if (typeof type === 'function') {
let ComponentClass = type;
let ownProps = getPropsFromChild(component);
const { state } = context;

// see if this is a connect type
if (typeof type.mapQueriesToProps === 'function') {
const data = type.mapQueriesToProps({ ownProps, state });
for (let key in data) {
if (!data.hasOwnProperty(key)) continue;

ownProps[key] = assign({}, defaultReactProps);
if (data[key].ssr === false) continue; // don't run this on the server

queries.push({
query: data[key],
component: type.WrappedComponent,
key,
ownProps,
context,
});
}

ComponentClass = type.WrappedComponent;
}

const Component = new ComponentClass(ownProps, context);

let newContext = context;
if (Component.getChildContext) newContext = assign({}, context, Component.getChildContext());

if (!store && ownProps.store) store = ownProps.store;
if (!store && newContext.store) store = newContext.store;

if (!client && ownProps.client && ownProps.client instanceof ApolloClient) {
client = ownProps.client as ApolloClient;
}
if (!client && newContext.client && newContext.client instanceof ApolloClient) {
client = newContext.client as ApolloClient;
}

getQueriesFromTree({
component: getChildFromComponent(Component),
context: newContext,
queries,
});
} else if (props && props.children) {
Children.forEach(props.children, (child: any) => getQueriesFromTree({
component: child,
context,
queries,
}));
}

return { queries, client, store };
}

// XXX component Cache
export function getDataFromTree(app, ctx: any = {}): Promise<any> {

let { client, store, queries } = getQueriesFromTree({ component: app, context: ctx });

if (!store && client && !client.store) client.initStore();
if (!store && client && client.store) store = client.store;
// no client found, nothing to do
if (!client || !store) return Promise.resolve(null);

// no queries found, nothing to do
if (!queries.length) return Promise.resolve({ store, client, initialState: store.getState() });

// run through all queries we can
return processQueries(queries, client)
.then(trees => Promise.all(trees.map(x => {
const { component, ownProps, context } = x;
if (!component) return;
// Traverse wrapped components of resulting queries
// NOTE: sub component queries may fire again,
// but they will just return back existing data
const Element = createElement(component, ownProps) as any;
const child = getChildFromComponent(Element && new Element.type(ownProps, context));
if (!child) return;

// traverse children nodes
return getDataFromTree(child, context);
})))
.then(() => ({ store, client, initialState: store.getState() }));

}

export function renderToStringWithData(component) {
return getDataFromTree(component)
.then(({ store, client }) => {
let markup = ReactDOM.renderToString(component);
let initialState = store.getState();
const key = client.reduxRootKey;
// XXX apollo client requires a lot in the store
// can we make this samller?
for (let queryId in initialState[key].queries) {
let fieldsToNotShip = ['minimizedQuery', 'minimizedQueryString'];
for (let field of fieldsToNotShip) delete initialState[key].queries[queryId][field];
}
initialState = encodeURI(JSON.stringify(initialState));
const payload = `<script>window.__APOLLO_STATE__ = ${initialState};</script>`;
markup += payload;
return markup;
});
}
Loading

0 comments on commit 2847115

Please sign in to comment.