Skip to content
Open
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
Empty file added .flowconfig
Empty file.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"eslint": "^3.8.1",
"eslint-plugin-babel": "^4.0.1",
"eslint-plugin-react": "^6.4.1",
"flow-bin": "^0.40.0",
"koa": "^1.2.4",
"mocha": "^3.2.0",
"node-fetch": "^1.6.3",
Expand Down
1 change: 1 addition & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
// @flow
export const __REACT_PREPARE__ = '@__REACT_PREPARE__@';
15 changes: 13 additions & 2 deletions src/dispatched.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @flow
import { PropTypes } from 'react';

import prepared from './prepared';
Expand All @@ -6,8 +7,18 @@ const storeShape = PropTypes.shape({
dispatch: PropTypes.func.isRequired,
});

const dispatched = (prepareUsingDispatch, opts = {}) => (OriginalComponent) => {
const prepare = (props, { store: { dispatch } }) => prepareUsingDispatch(props, dispatch);
type Opts = {
componentDidMount?: boolean,
componentWillReceiveProps?: boolean,
contextTypes?: Object,
pure?: boolean,
};

const dispatched = (prepareUsingDispatch: Function, opts: Opts = {}) => (
OriginalComponent: Function,
) => {
const prepare = (props, { store: { dispatch } }) =>
prepareUsingDispatch(props, dispatch);
const contextTypes = Object.assign(
{},
opts && opts.contextTypes ? opts.contextTypes : {},
Expand Down
7 changes: 2 additions & 5 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
// @flow
import dispatched from './dispatched';
import prepare from './prepare';
import prepared from './prepared';

export {
dispatched,
prepare,
prepared,
};
export { dispatched, prepare, prepared };

export default prepare;
38 changes: 23 additions & 15 deletions src/prepare.js
Original file line number Diff line number Diff line change
@@ -1,61 +1,69 @@
// @flow
import React from 'react';

import isReactCompositeComponent from './utils/isReactCompositeComponent';
import isThenable from './utils/isThenable';
import { isPrepared, getPrepare } from './prepared';

function createCompositeElementInstance({ type: CompositeComponent, props }, context) {
function createCompositeElementInstance(
{ type: CompositeComponent, props },
context,
) {
const instance = new CompositeComponent(props, context);
if(instance.componentWillMount) {
if (instance.componentWillMount) {
instance.componentWillMount();
}
return instance;
}

function renderCompositeElementInstance(instance, context = {}) {
const childContext = Object.assign({}, context, instance.getChildContext ? instance.getChildContext() : {});
const childContext = Object.assign(
{},
context,
instance.getChildContext ? instance.getChildContext() : {},
);
return [instance.render(), childContext];
}

function disposeOfCompositeElementInstance() {
}
function disposeOfCompositeElementInstance() {}

async function prepareCompositeElement({ type, props }, context) {
if(isPrepared(type)) {
if (isPrepared(type)) {
const p = getPrepare(type)(props, context);
if(isThenable(p)) {
if (isThenable(p)) {
await p;
}
}
let instance = null;
try {
instance = createCompositeElementInstance({ type, props }, context);
return renderCompositeElementInstance(instance, context);
}
finally {
if(instance !== null) {
} finally {
if (instance !== null) {
disposeOfCompositeElementInstance(instance);
}
}
}

async function prepareElement(element, context) {
if(element === null || typeof element !== 'object') {
if (element === null || typeof element !== 'object') {
return [null, context];
}
const { type, props } = element;
if(typeof type === 'string') {
if (typeof type === 'string') {
return [props.children, context];
}
if(!isReactCompositeComponent(type)) {
if (!isReactCompositeComponent(type)) {
return [type(props), context];
}
return await prepareCompositeElement(element, context);
}

const prepare = async (element, context = {}) => {
const prepare = async (element: any, context: Object = {}) => {
const [children, childContext] = await prepareElement(element, context);
await Promise.all(React.Children.toArray(children).map((child) => prepare(child, childContext)));
await Promise.all(
React.Children.toArray(children).map(child => prepare(child, childContext)),
);
};

export default prepare;
21 changes: 13 additions & 8 deletions src/prepared.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
// @flow weak
import React, { PureComponent, Component } from 'react';

import { __REACT_PREPARE__ } from './constants';

const prepared = (prepare, {
pure = true,
componentDidMount = true,
componentWillReceiveProps = true,
contextTypes = {},
} = {}) => (OriginalComponent) => {
const prepared = (
prepare,
{
pure = true,
componentDidMount = true,
componentWillReceiveProps = true,
contextTypes = {},
} = {},
) => OriginalComponent => {
const { displayName } = OriginalComponent;
class PreparedComponent extends (pure ? PureComponent : Component) {
static displayName = `PreparedComponent${displayName ? `(${displayName})` : ''}`;
Expand All @@ -16,13 +20,13 @@ const prepared = (prepare, {
static contextTypes = contextTypes;

componentDidMount() {
if(componentDidMount) {
if (componentDidMount) {
prepare(this.props, this.context);
}
}

componentWillReceiveProps(nextProps, nextContext) {
if(componentWillReceiveProps) {
if (componentWillReceiveProps) {
prepare(nextProps, nextContext);
}
}
Expand All @@ -31,6 +35,7 @@ const prepared = (prepare, {
return <OriginalComponent {...this.props} />;
}
}
// $FlowFixMe
PreparedComponent[__REACT_PREPARE__] = prepare.bind(null);
return PreparedComponent;
};
Expand Down
33 changes: 22 additions & 11 deletions src/tests/ReactLifeCycle.spec.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @flow
const { describe, it } = global;
import React, { Component, PropTypes } from 'react';
import { renderToString } from 'react-dom/server';
Expand Down Expand Up @@ -29,20 +30,30 @@ describe('React lifecycle methods', () => {
it('renderToString calls #componentWillMount()', () => {
const spyForComponentWillMount = sinon.spy();
const spyForComponentWillUnmount = () => void 0;
renderToString(<CompositeComponent
spyForComponentWillMount={spyForComponentWillMount}
spyForComponentWillUnmount={spyForComponentWillUnmount}
/>);
t.assert(spyForComponentWillMount.calledOnce, '#componentWillMount() has been called once');
renderToString(
<CompositeComponent
spyForComponentWillMount={spyForComponentWillMount}
spyForComponentWillUnmount={spyForComponentWillUnmount}
/>,
);
t.assert(
spyForComponentWillMount.calledOnce,
'#componentWillMount() has been called once',
);
});

it('renderToString doesn\'t call #componentWillUnmount()', () => {
it("renderToString doesn't call #componentWillUnmount()", () => {
const spyForComponentWillMount = () => void 0;
const spyForComponentWillUnmount = sinon.spy();
renderToString(<CompositeComponent
spyForComponentWillMount={spyForComponentWillMount}
spyForComponentWillUnmount={spyForComponentWillUnmount}
/>);
t.assert(spyForComponentWillUnmount.callCount === 0, '#componentWillUnmount() has not been called');
renderToString(
<CompositeComponent
spyForComponentWillMount={spyForComponentWillMount}
spyForComponentWillUnmount={spyForComponentWillUnmount}
/>,
);
t.assert(
spyForComponentWillUnmount.callCount === 0,
'#componentWillUnmount() has not been called',
);
});
});
70 changes: 39 additions & 31 deletions src/tests/dispatched.spec.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @flow
const { describe, it } = global;
import url from 'url';
import t from 'tcomb';
Expand All @@ -19,7 +20,6 @@ const HTTP_STATUS_OK_BOUNDS = {

describe('dispatched', () => {
it('Real-world-like example using redux, koa, et. al', async () => {

// Create a fake echo server that replies with the pathname, preceded by 'echo '.
const echoServer = koa().use(function* echo(next) {
this.response.body = `echo ${this.request.path}`;
Expand All @@ -39,15 +39,15 @@ describe('dispatched', () => {
const FETCH_SUCCEEDED = 'FETCH_SUCCEEDED';

const rootReducer = (state = {}, { type, ...payload }) => {
if(type === FETCH_STARTED) {
if (type === FETCH_STARTED) {
const { into } = payload;
return Object.assign({}, state, {
[into]: {
status: FETCH_STARTED,
},
});
}
if(type === FETCH_FAILED) {
if (type === FETCH_FAILED) {
const { into, statusCode, err } = payload;
return Object.assign({}, state, {
[into]: {
Expand All @@ -57,7 +57,7 @@ describe('dispatched', () => {
},
});
}
if(type === FETCH_SUCCEEDED) {
if (type === FETCH_SUCCEEDED) {
const { into, value } = payload;
return Object.assign({}, state, {
[into]: {
Expand All @@ -70,23 +70,21 @@ describe('dispatched', () => {
};

// redux store used by the app
const store = createStore(
rootReducer,
applyMiddleware(
thunkMiddleware,
),
);
const store = createStore(rootReducer, applyMiddleware(thunkMiddleware));

// async action creator
const fetchInto = (pathname, into) => async (dispatch) => {
const fetchInto = (pathname, into) => async dispatch => {
dispatch({
type: FETCH_STARTED,
into,
});
const href = url.format(Object.assign({}, baseUrlObj, { pathname }));
try {
const res = await fetch(href);
if(res.status < HTTP_STATUS_OK_BOUNDS.min || res.status >= HTTP_STATUS_OK_BOUNDS.max) {
if (
res.status < HTTP_STATUS_OK_BOUNDS.min ||
res.status >= HTTP_STATUS_OK_BOUNDS.max
) {
dispatch({
type: FETCH_FAILED,
into,
Expand All @@ -101,8 +99,7 @@ describe('dispatched', () => {
value: await res.text(),
});
return;
}
catch(err) {
} catch (err) {
dispatch({
type: FETCH_FAILED,
into,
Expand All @@ -113,14 +110,14 @@ describe('dispatched', () => {
};

const OriginalEchoAlpha = ({ alpha }) => {
if(typeof alpha !== 'object') {
if (typeof alpha !== 'object') {
return <div>???</div>;
}
const { status, err, value } = alpha;
if(status === FETCH_STARTED) {
if (status === FETCH_STARTED) {
return <div>...</div>;
}
if(status === FETCH_FAILED) {
if (status === FETCH_FAILED) {
return <div>Error fetching beta (Reason: {err})</div>;
}
return <div>{value}</div>;
Expand All @@ -129,19 +126,22 @@ describe('dispatched', () => {
alpha: PropTypes.object,
};

const ConnectedEchoAlpha = connect(({ alpha }) => ({ alpha }))(OriginalEchoAlpha);
const ConnectedEchoAlpha = connect(({ alpha }) => ({ alpha }))(
OriginalEchoAlpha,
);

const EchoAlpha = dispatched(({ value }, dispatch) => dispatch(fetchInto(value, 'alpha')))(ConnectedEchoAlpha);
const EchoAlpha = dispatched(({ value }, dispatch) =>
dispatch(fetchInto(value, 'alpha')))(ConnectedEchoAlpha);

const OriginalEchoBeta = ({ beta }) => {
if(typeof beta !== 'object') {
if (typeof beta !== 'object') {
return <div>???</div>;
}
const { status, err, value } = beta;
if(status === FETCH_STARTED) {
if (status === FETCH_STARTED) {
return <div>...</div>;
}
if(status === FETCH_FAILED) {
if (status === FETCH_FAILED) {
return <div>Error fetching beta (Reason: {err})</div>;
}
return <div>{value}</div>;
Expand All @@ -150,22 +150,30 @@ describe('dispatched', () => {
beta: PropTypes.object,
};

const ConnectedEchoBeta = connect(({ beta }) => ({ beta }))(OriginalEchoBeta);
const ConnectedEchoBeta = connect(({ beta }) => ({ beta }))(
OriginalEchoBeta,
);

const EchoBeta = dispatched(({ value }, dispatch) => dispatch(fetchInto(value, 'beta')))(ConnectedEchoBeta);
const EchoBeta = dispatched(({ value }, dispatch) =>
dispatch(fetchInto(value, 'beta')))(ConnectedEchoBeta);

const App = () => <ul>
<li key='alpha'><EchoAlpha value='foo' /></li>
<li key='beta'><EchoBeta value='bar' /></li>
</ul>;
const App = () => (
<ul>
<li key="alpha"><EchoAlpha value="foo" /></li>
<li key="beta"><EchoBeta value="bar" /></li>
</ul>
);

const app = <Provider store={store}><App /></Provider>;

await prepare(app);
const html = renderToStaticMarkup(app);
t.assert(html === '<ul><li><div>echo /foo</div></li><li><div>echo /bar</div></li></ul>', 'renders correct html');
}
finally {
t.assert(
html ===
'<ul><li><div>echo /foo</div></li><li><div>echo /bar</div></li></ul>',
'renders correct html',
);
} finally {
echoHttpServer.close();
}
});
Expand Down
Loading