From 3a1e5195a4c2674cf30c81bd89095ad6194fd88b Mon Sep 17 00:00:00 2001 From: Nikolay Date: Wed, 11 Jan 2017 05:46:02 +0300 Subject: [PATCH] Releasing 9.0.0. Rewrote the library without using `redux-router` + a couple of breaking changes (see HISTORY) --- .babelrc | 34 +- CACHING.md | 2 +- HISTORY.md | 25 +- README.md | 476 +++++++++++++----- index.common.js | 24 +- index.es6.js | 42 +- package.json | 9 +- redux.js | 24 - server.js | 3 + source/page-server/render.js | 81 +-- source/page-server/web server.js | 9 +- source/react-router/client.js | 4 +- source/redux/Link.js | 103 ++++ source/redux/asynchronous action handler.js | 207 ++++++++ source/redux/client/client.js | 64 +-- source/redux/client/create http client.js | 13 + source/redux/client/create store.js | 32 ++ source/redux/client/render.js | 110 ++-- source/redux/client/store.js | 5 +- .../middleware/asynchronous middleware.js | 6 +- source/redux/middleware/history middleware.js | 22 + .../middleware/on route update middleware.js | 85 ++-- .../redux/middleware/preloading middleware.js | 320 +++++++----- source/redux/naming.js | 33 ++ source/redux/normalize.js | 35 +- source/redux/on enter.js | 60 --- source/redux/redux-router/README.md | 9 + source/redux/redux-router/ReduxRouter.js | 111 ++++ source/redux/redux-router/actionCreators.js | 63 +++ .../redux-router/addHistorySynchronization.js | 72 +++ source/redux/redux-router/client.js | 25 + source/redux/redux-router/constants.js | 13 + .../redux/redux-router/historyMiddleware.js | 15 + source/redux/redux-router/index.js | 14 + source/redux/redux-router/isActive.js | 17 + source/redux/redux-router/matchMiddleware.js | 17 + source/redux/redux-router/reduxReactRouter.js | 63 +++ .../redux/redux-router/routerStateEquals.js | 18 + .../redux/redux-router/routerStateReducer.js | 27 + source/redux/redux-router/server.js | 64 +++ source/redux/redux-router/useDefaults.js | 21 + source/redux/server/create http client.js | 17 + source/redux/server/create store.js | 31 ++ source/redux/server/render.js | 86 ++-- source/redux/server/store.js | 6 +- source/redux/store.js | 126 +++-- test/exports.js | 51 +- 47 files changed, 1974 insertions(+), 720 deletions(-) delete mode 100644 redux.js create mode 100644 source/redux/Link.js create mode 100644 source/redux/asynchronous action handler.js create mode 100644 source/redux/client/create http client.js create mode 100644 source/redux/client/create store.js create mode 100644 source/redux/middleware/history middleware.js create mode 100644 source/redux/naming.js delete mode 100644 source/redux/on enter.js create mode 100644 source/redux/redux-router/README.md create mode 100644 source/redux/redux-router/ReduxRouter.js create mode 100644 source/redux/redux-router/actionCreators.js create mode 100644 source/redux/redux-router/addHistorySynchronization.js create mode 100644 source/redux/redux-router/client.js create mode 100644 source/redux/redux-router/constants.js create mode 100644 source/redux/redux-router/historyMiddleware.js create mode 100644 source/redux/redux-router/index.js create mode 100644 source/redux/redux-router/isActive.js create mode 100644 source/redux/redux-router/matchMiddleware.js create mode 100644 source/redux/redux-router/reduxReactRouter.js create mode 100644 source/redux/redux-router/routerStateEquals.js create mode 100644 source/redux/redux-router/routerStateReducer.js create mode 100644 source/redux/redux-router/server.js create mode 100644 source/redux/redux-router/useDefaults.js create mode 100644 source/redux/server/create http client.js create mode 100644 source/redux/server/create store.js diff --git a/.babelrc b/.babelrc index 0dac709..6a29b3f 100644 --- a/.babelrc +++ b/.babelrc @@ -1,20 +1,7 @@ { - "presets": - [ - "react", - "es2015", - "stage-0" - ], - - "plugins": - [ - "transform-runtime", - "transform-react-display-name" - ], - "env": { - "commonjs": + "development": { "presets": [ @@ -29,6 +16,22 @@ "transform-react-display-name" ] }, + "commonjs": + { + "presets": + [ + "react", + "es2015", + "stage-0" + ], + + "plugins": + [ + "transform-runtime", + "transform-react-display-name", + "transform-decorators-legacy" + ] + }, "es6": { "presets": @@ -41,7 +44,8 @@ "plugins": [ "transform-runtime", - "transform-react-display-name" + "transform-react-display-name", + "transform-decorators-legacy" ] } } diff --git a/CACHING.md b/CACHING.md index b66d3f4..cacd1e8 100644 --- a/CACHING.md +++ b/CACHING.md @@ -32,7 +32,7 @@ Each entity has a visual representation (e.g. a thread has a `
  • ...
  • ` repr This is a good example of highload architecture, and it would work in React too, but React is currently cumbersome when it comes to partial caching tricks (e.g. caching a header and a footer while rendering only the content part) because of having those `react-dataid` and `react-data-checksum` attributes guarding the resulting HTML markup integrity. I.e. one can't just render the content of a page and then inject the already cached header and footer markup there because then `react-dataid` counters would be messed up and `react-data-checksum` wouldn't match, so React would just discard the whole server-rendered markup and rerender it from scratch in the web browser, which is stupid. See [this discussion in `facebook/react` repo](https://github.com/facebook/react/issues/5869#issuecomment-250967382) for more info. Facebook seems not interested in fixing this issue since they're not using server-side rendering at all. There was [an effort](https://github.com/aickin/react-dom-stream) to fork React and fix that particular disability but the contributor obviously didn't manage to keep up with the pace and the project got stuck at React 0.14 and was finally abandoned. -Ok, so composing a webpage from prerendered cached bits of markup isn't currently an option with React. Still, the full page could be cached. +There is however an interesting project out of Walmart Labs worth checking out: [`react-ssr-optimization`](https://github.com/walmartlabs/react-ssr-optimization). It takes another approach from `react-dom-stream` and istead of forking and patching React library itself it injects some bootstrapping code inside it via a Node.js `require()` hook. This might turn out to be a good solution (I haven't tried it myself). ## Caching the whole page diff --git a/HISTORY.md b/HISTORY.md index 94d87ee..05b7c14 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,24 @@ +9.0.0 / 10.01.2017 +================== + + A couple of TODOs for `9.0.0`: + + * Clean up the new code - remove commented out pieces left from `8.0.0` + * Fix `onEnter` being called twice (both on server and client) - this is not considered a blocker for `9.0.0` release since `@preload()` supercedes `onEnter` and therefore `onEnter` may not be used at all. But I'll look into it. + * Maybe implement the minor `previous_route_components` optimization from `8.0.0` for preloading pages + + Changes: + + * Added "asynchronous action handlers" (see README) + + * (breaking change) Removed `redux-router` out of this library. Use `import { Link } from 'react-isomorphic-render'` instead of `import { Link } from 'react-router'` + * (breaking change) `import` everything from `react-isomorphic-render` now, not from `react-isomorphic-render/redux` + * (breaking change) Changed the order of arguments for `render()` and `pageRenderingService()`: they both now take the common settings first, then the specific settings. Migration: `render({...}, settigs)` -> `render(settings, {...})`, `pageRenderingService({...}, settings)` -> `pageRenderingService(settings, {...})` + * (breaking change) Removed `onStoreCreated` due to it not being used anymore (Redux reducers hot reload is now moved to `application.js` client-side main file) + * (breaking change) `@onEnter` workaround helper is no longer neccessary because I fixed the `redux-router` `onEnter` bug + * (breaking change) `onNavigate` moved to client-side `render()` function parameters + * Renamed `promise_event_naming` to `asynchronous_action_event_naming` and added a camelCase alias. And it no longer has a default. + 8.0.13 / 08.01.2017 =================== @@ -201,10 +222,10 @@ 4.1.2 / 20.08.2016 =================== - * Slightly changed the behaviour of the undocumented `event` parameter of `asynchronous_middleware`: now it transforms `event` into an array of `[event: pending, event: done, event: failed]` as opposed to the older colonless `[event pending, event done, event failed]`. This could break things for those who were using this undocumented feature, but an easy hotfix is to provide `promise_event_naming` function parameter in `common.js` to retain the old Redux event naming scheme: + * Slightly changed the behaviour of the undocumented `event` parameter of `asynchronous_middleware`: now it transforms `event` into an array of `[event: pending, event: done, event: failed]` as opposed to the older colonless `[event pending, event done, event failed]`. This could break things for those who were using this undocumented feature, but an easy hotfix is to provide `asynchronous_action_event_naming` function parameter in `common.js` to retain the old Redux event naming scheme: ```js -promise_event_naming(event_name) +asynchronous_action_event_naming(event_name) { return [`${event_name} pending`, `${event_name} done`, `${event_name} failed`] } diff --git a/README.md b/README.md index 167518e..d28a596 100644 --- a/README.md +++ b/README.md @@ -47,8 +47,7 @@ $ npm install react-isomorphic-render --save Start by creating your `react-isomorphic-render.js` set up file (it configures both client side and server side) ```javascript -export default -{ +export default { // Redux reducer // (either a reducer or a function returning a reducer) reducer: require('./src/client/redux/reducer'), @@ -59,6 +58,7 @@ export default // returning a `` element) routes: require('./src/client/routes'), + // A React component. // Wraps React page component with arbitrary elements // (e.g. Redux , and other "context providers") wrapper: require('./src/client/wrapper') @@ -74,23 +74,22 @@ import { Provider } from 'react-redux' export default class Wrapper extends React.Component { render() { const { store, children } = this.props - return {children} + return { children } } } ``` -Then create your client-side application main file (`application.js`) +Then create your client-side main application file (`application.js`) ```javascript -import { render } from 'react-isomorphic-render' +// Include CSS styles in the bundle +require('../styles/main.css') +import { render } from 'react-isomorphic-render' import settings from './react-isomorphic-render' -// Include styles in the bundle -require('../styles/main.css') - -// Renders the page in web browser -render({}, settings) +// Render the page in web browser +render(settings) ``` And the `index.html` would look like this: @@ -132,7 +131,7 @@ import settings from './react-isomorphic-render' const maxAge = 365 * 24 * 60 * 60; // Create webpage rendering server -const server = webpageServer({ +const server = webpageServer(settings, { // HTTP host and port for performing all AJAX requests // when rendering pages on server-side. // E.g. an AJAX request to `/items/5` will be transformed to @@ -160,8 +159,7 @@ const server = webpageServer({ // Inserted before page rendering middleware. // Adjust the path to the Webpack `build` folder. middleware: [mount('/assets', statics(path.join(__dirname, '../build'), { maxAge }))] -}, -settings) +}) // Start webpage rendering server on port 3000 // (`server.listen(port, [host], [callback])`) @@ -178,7 +176,7 @@ server.listen(3000, function(error) { ```js import http from 'http' import webpageServer from 'react-isomorphic-render/server' -const server = webpageServer({...}) +const server = webpageServer(settings, {...}) http.createServer(server.callback()).listen(3000, error => ...) ``` @@ -187,7 +185,7 @@ And for HTTPS websites start the page server like this: ```js import https from 'https' import webpageServer from 'react-isomorphic-render/server' -const server = webpageServer({...}) +const server = webpageServer(settings, {...}) https.createServer(options, server.callback()).listen(3001, error => ...) ``` @@ -233,10 +231,12 @@ app.use(function(request, response) { }) ``` -For production usage something like the [NginX proxy](https://www.sep.com/sep-blog/2014/08/20/hosting-the-node-api-in-nginx-with-a-reverse-proxy/) is a better solution. +For production usage something like the [NginX proxy](https://www.sep.com/sep-blog/2014/08/20/hosting-the-node-api-in-nginx-with-a-reverse-proxy/) is a better solution (both for proxying and for serving static files). ## Without proxying +(Advanced section, may be skipped) + To use `react-isomorphic-render` without proxying there are two options * Either supply custom Koa `middleware` array option in webpage server configuration @@ -244,6 +244,7 @@ To use `react-isomorphic-render` without proxying there are two options ```js import { render } from 'react-isomorphic-render/server' +import settings from './react-isomorphic-render' try { // Returns a Promise. @@ -252,7 +253,7 @@ try { // content - rendered HTML document (markup) // redirect - redirection URL (in case of HTTP redirect) // - const { status, content, redirect } = await render({ + const { status, content, redirect } = await render(settings, { // Takes the same parameters as webpage server application: { host, port }, assets, @@ -267,9 +268,7 @@ try { // The rest optional parameters are the same // as for webpage server and are all optional - }, - // The second `settings` parameter is the same as for webpage server - settings) + }) if (redirect) { return redirect_to(redirect) @@ -298,11 +297,13 @@ Example: function asynchronousAction() { return { promise: () => Promise.resolve({ success: true }), - events: ['PROMISE_PENDING', 'PROMISE_SUCCESS', 'PROMISE_FAILURE'] + events: ['PROMISE_PENDING', 'PROMISE_SUCCESS', 'PROMISE_ERROR'] } } ``` +When you find yourself copy-pasting those `_PENDING`, `_SUCCESS` and `_ERROR` event names from one action creator to another then take a look at `asynchronousActionEventNaming` setting described in the [Additional `react-isomorphic-render.js` settings](https://github.com/halt-hammerzeit/react-isomorphic-render#additional-react-isomorphic-renderjs-settings) section of this document. + ### HTTP utility For convenience, the argument of the `promise` function is the built-in `http` utility having methods `get`, `head`, `post`, `put`, `patch`, `delete`, each returning a `Promise` and taking three arguments: the `url` of the HTTP request, `parameters` object, and an `options` object. It can be used to easily query HTTP REST API endpoints in Redux action creators. @@ -386,9 +387,8 @@ By default, when using `http` utility all JSON responses get parsed for javascri For page preloading consider using `@preload()` helper to load the neccessary data before the page is rendered. ```javascript -import { connect } from 'react-redux' -import { bindActionCreators } from 'redux' -import { title, preload } from 'react-isomorphic-render' +import { connect } from 'react-redux' +import { title, preload } from 'react-isomorphic-render' // fetches the list of users from the server function fetchUsers() { @@ -400,44 +400,20 @@ function fetchUsers() { @preload(({ dispatch }) => dispatch(fetchUsers)) @connect( - state => ({ users: state.users.users }), - dispatch => bindActionCreators({ fetchUsers }, dispatch) + state => ({ users: state.users.users }), + { fetchUsers }) ) export default class Page extends Component { - static propTypes = { - users : PropTypes.array.isRequired, - fetchUsers : PropTypes.func.isRequired - } - render() { + const { users, fetchUsers } = this.props return (
    - {title("Users")} -
      {this.props.users.map(user =>
    • {user.name}
    • )}
    - + { title("Users") } +
      { users.map(user =>
    • { user.name }
    • ) }
    +
    ) } - - // Observing action result example (advanced). - // - // Suppose you make a `deleteUsers()` function - // analagous to the `fetchUsers()` function. - // - // Then you can call it like this: - // - // - // - // (async/await Babel syntax is used here; can be rewritten as usual Promises) - // - async deleteUsers() { - try { - const count = await this.props.deleteUsers() - alert(`Deleted ${count} users`) - } catch (error) { - alert(error) - } - } } ``` @@ -447,27 +423,40 @@ In the example above `@preload()` helper is called to preload a web page before @preload(function({ dispatch, getState, location, parameters }) { return Promise }) ``` -Or, using `async/await`: +When `dispatch` is called with a special "asynchronous" action (having `promise` and `events` properties) then such a `dispatch()` call will return a `Promise`, that's why in the example above it's written as: -```javascript -@preload(async ({ dispatch, getState, location, parameters }) => await doSomething()) +```js +@preload(({ dispatch }) => dispatch(fetchUsers)) ``` -When `dispatch` is called with a special "asynchronous" action (having `promise` and `events` properties) then such a `dispatch()` call will return a `Promise`, that's why in the example above it's written as: +Note: `transform-decorators-legacy` Babel plugin is needed at the moment to make decorators work in Babel + +```sh +npm install babel-plugin-transform-decorators-legacy --save +``` + +#### .babelrc ```js -@preload(({ dispatch }) => dispatch(fetchUsers)) +{ + "presets": [ + ... + ], + "plugins": [ + "transform-decorators-legacy" + ] +} ``` -Note: if `@preload()` decorator seems not working (though it definitely should) then try to place it on top of all other decorators. The possible reason is that it adds a static method to your `Route`'s `component` and some decorator on top of it may not retain that static method (though all proper decorators are agreed to retain static methods and variables). +P.S.: if `@preload()` decorator seems not working (though it definitely should) then try to place it on top of all other decorators. The possible reason is that it adds a static method to your `Route`'s `component` and some decorator on top of it may not retain that static method (though all proper decorators are agreed to retain static methods and variables). On the client side, when a user navigates a link, first it changes the URL in the address bar, then it waits for the next page to preload, and, when the next page is fully loaded, then it is displayed to the user. Sometimes preloading a page can take some time to finish so one may want to add a "spinner" to inform the user that the application isn't frozen and the navigation process needs some time to finish. This can be achieved by adding a Redux reducer listening to these three Redux events: ```javascript import { PRELOAD_STARTED, PRELOAD_FINISHED, PRELOAD_FAILED } from 'react-isomorphic-render' -export default function(state = {}, event = {}) { - switch (event.type) { +export default function(state = {}, action = {}) { + switch (action.type) { case PRELOAD_STARTED : return { ...state, pending: true, error: false } case PRELOAD_FINISHED : return { ...state, pending: false } case PRELOAD_FAILED : return { ...state, pending: false, error: event.error } @@ -482,10 +471,10 @@ And a "spinner" component import React from 'react' import { connect } from 'react-redux' -export default connect(state => ({ pending: state.preload.pending })) -(function Spinner(props) { - return
    -}) +@connect(state => ({ pending: state.preload.pending })) +export default function Spinner(props) { + return
    +} ``` ```css @@ -500,7 +489,7 @@ export default connect(state => ({ pending: state.preload.pending })) display: none; } -.preloading-show { +.preloading--shown { display: block; cursor: wait; } @@ -553,6 +542,182 @@ const meta = [ { meta({ ... same `meta` as above ... }) } ``` +### Handling asynchronous actions + +Once one starts writing a lot of Ajax calls in Redux it becomes obvious that there's **a lot** of boilerplate copy-pasting involved. To reduce those tremendous amounts of copy-pasta an "asynchronous action handler" may be used: + +#### redux/blogPost.js + +```js +import { action, createHandler, stateConnector } from 'react-isomorphic-render' +// (`./react-isomorphic-render-async.js` settings file is described below) +import settings from './react-isomorphic-render-async' + +const handler = createHandler(settings) + +// Post comment Redux "action creator" +export const postComment = action({ + namespace: 'BLOG_POST', + event: 'POST_COMMENT', + promise(userId, blogPostId, commentText, http) { + return http.post(`/blog/posts/${blogPostId}/comment`, { + userId: userId, + text: commentText + }) + } +}, +handler) + +// Get comments Redux "action creator" +export const getComments = action({ + namespace: 'BLOG_POST', + event: 'GET_COMMENTS', + promise(blogPostId, http) { + return http.get(`/blog/posts/${blogPostId}/comments`) + }, + // The fetched comments will be placed + // into the `comments` Redux state property. + result: 'comments' + // Or write it as a reducer: + // result: (state, result) => ({ ...state, comments: result }) +}, +handler) + +// This is for the Redux `@connect()` helper below +handler.addStateProperties('comments') + +// A developer can additionally handle any other custom events +handler.handle(eventName('BLOG_POST', 'CUSTOM_EVENT'), (state, action) => ({ + ...state, + customProperty: action.result +})) + +// This is for the Redux `@connect()` helper below +handler.addStateProperties('customProperty') + +// A little helper for Redux `@connect()` +export const connector = stateConnector(handler) + +// This is the Redux reducer which now +// handles the asynchronous actions defined above. +export default handler.reducer() +``` + +#### redux/reducer.js + +```js +export { default as blogPost } from './blogPost' +... +``` + +And a React component would look like this + +```js +import React, { Component } from 'react' +import { connect } from 'react-redux' +import { preload } from 'react-isomorphic-render' +import { connector, getComments, postComment } from './redux/blogPost' + +// Preload comments before showing the page +// (see "Page preloading" section of this document) +@preload(({ dispatch, getState, parameters }) => { + return dispatch(getComments(parameters.blogPostId)) +}) +// See `react-redux` documentation on `@connect()` decorator +@connect((state) => ({ + userId: state.user.id, + // `connector` will populate the Redux `props` + ...connector(state.blogPost) +}), { + postComment +}) +export default class BlogPostPage extends Component { + render() { + const { + getCommentsPending, + getCommentsError, + comments + } = this.props + + if (getCommentsError) { + return
    Couldn't load comments
    + } + + return ( +
    +
      + { comments.map(comment =>
    • {comment}
    • ) } +
    + {this.renderPostCommentForm()} +
    + ) + } + + renderPostCommentForm() { + const { + userId, + params, + postComment, + postCommentPending, + postCommentError + } = this.props + + if (postCommentPending) { + return
    Posting comment...
    + } + + if (postCommentError) { + return
    Couldn't post comment
    + } + + return ( + + ) + } +} +``` + +And the additional configuration would be: + +#### react-isomorphic-render.js + +```js +import asyncSettings from './react-isomorphic-render-async' + +export default { + // All the settings as before + + ...asyncSettings +} +``` + +#### react-isomorphic-render-async.js + +```js +import { underscoredToCamelCase } from 'react-isomorphic-render' + +export default { + // When supplying `event` instead of `events` + // as part of an asynchronous Redux action + // this will generate `events` from `event` + // using this function. + asynchronousActionEventNaming: event => ([ + `${event}_PENDING`, + `${event}_SUCCESS`, + `${event}_ERROR` + ]), + + // When using "asynchronous action handlers" feature + // this function will generate a Redux state property name from an event name. + // E.g. event `GET_USERS_ERROR` => state.`getUsersError`. + asynchronousActionHandlerStatePropertyNaming: underscoredToCamelCase, +} +``` + +Notice the extraction of these two configuration parameters into a separate file `react-isomorphic-render-async.js`: it is done to break circular dependency on `./react-isomorphic-render.js` file because `routes` `import` React page components which in turn `import` action creators which in turn import `./react-isomorphic-render.js` hence the circular (recursive) dependency (same goes for `reducer`). + ### Locale detection This library performs the following locale detection steps for each webpage rendering HTTP request: @@ -580,29 +745,22 @@ These two helper Redux actions change the current location (both on client and s ```javascript import { goto, redirect } from 'react-isomorphic-render' +import { connect } from 'react-redux' // Usage example // (`goto` navigates to a URL while adding a new entry in browsing history, // `redirect` does the same replacing the current entry in browsing history) -return this.props.dispatch(goto('/items/1?color=red')) +@connect(state = {}, { goto, redirect }) +class Page extends Component { + handleClick(event) { + return this.props.goto('/items/1?color=red') + // return this.props.redirect('/somewhere') + } +} ``` A sidenote: these two functions aren't supposed to be used inside `onEnter` and `onChange` `react-router` hooks. Instead use the `replace` argument supplied to these functions by `react-router` when they are called (`replace` works the same way as `redirect`). -### onEnter - -Some people requested support for using `dispatch` and `getState` in `react-router`'s `onEnter` hooks. While [this Pull Request](https://github.com/acdlite/redux-router/pull/272) in `redux-router` repo has not been accepted yet use the `onEnter` helper to get this functionality: - -```js -import { onEnter } from 'react-isomorphic-render' - - - { - redirect('/somewhere') - })(UserProfile)}/> - -``` - ## Caching [Some thoughts on caching rendered pages](https://github.com/halt-hammerzeit/react-isomorphic-render/blob/master/CACHING.md) @@ -687,44 +845,96 @@ This library attempts to read authenication token from a cookie named `settings. ## Webpack HMR -Webpack's [Hot Module Replacement](https://webpack.github.io/docs/hot-module-replacement.html) (aka Hot Reload) works both on React components and Redux reducers. +Webpack's [Hot Module Replacement](https://webpack.github.io/docs/hot-module-replacement.html) (aka Hot Reload) works for React components and Redux reducers and Redux action creators. -For enabling HMR on Redux reducers first one should make sure that the `reducer` parameter is a function and that it's not simply `reducer: () => reducer` but is instead `reducer: () => require('./reducer')`, i.e. the `require()` call has to be inside the `reducer` parameter function itself, because that's how Webpack HMR works: explicit `require()`s must be placed inside `module.hot.accept` handler, not at the top of the file as usually (i.e. not `import reducer from './reducer'` — that wouldn't ever hot reload). +HMR setup for Redux reducers is as simple as adding `store.hotReload()` (as shown below). For enabling [HMR on React Components](https://webpack.js.org/guides/hmr-react/) (and Redux action creators) I would suggest the new [react-hot-loader 3](https://github.com/gaearon/react-hot-loader) (which is still in beta, so install it like `npm install react-hot-loader@3.0.0-beta.6 --save`): -After specifying the correct `reducer` parameter function the next step is to set up `onStoreCreated` handler like this: +#### application.js ```js -{ - ... +import settings from './react-isomorphic-render' + +render(settings).then(({ store, rerender }) => { + if (module.hot) { + // This path must be equal to the path + // inside the `require()` call in the `routes` parameter + module.hot.accept('./react-isomorphic-render', () => { + rerender() + // Update reducer (for Webpack 2 ES6) + store.hotReload(settings.reducer) + // Update reducer (for Webpack 1) + // store.hotReload(require('./react-isomorphic-render').reducer) + }) + } +}) +``` - reducer: () => require('./reducer'), +#### wrapper.js - onStoreCreated({ reloadReducer }) { - if (module.hot) { - // This path must be equal to the path - // inside the `require()` call in the `reducer` parameter - module.hot.accept('./reducer', reloadReducer) - } +```js +import React, { Component, PropTypes } from 'react' +import { Provider } from 'react-redux' +import { AppContainer } from 'react-hot-loader' + +export default class Wrapper extends Component { + static propTypes = { + store: React.PropTypes.object.isRequired + } + + render() { + const { store, children } = this.props; + + return ( + + + { children } + + + ) } } ``` -For enabling HMR on React Components I would suggest either the good-old [react-transform-hmr](https://github.com/gaearon/react-transform-hmr) (which I'm using myself) or the new [react-hot-loader 3](https://github.com/gaearon/react-hot-loader) (which is still in beta). Those who don't like either of them can try to set up React Components HMR manually and in that case the instructions would be the same as for Redux reducers above: first make sure that `routes` parameter is a function having `require()` calls for `` Components inside itself (not at the top of the file), and then use the `Promise` returned from the `render()` function call: +#### .babelrc + +```js +{ + "presets": [ + "react", + // For Webpack 2 ES6: + ["es2015", { modules: false }], + // For Webpack 1: + // "es2015", + "stage-2" + ], + + "plugins": [ + "react-hot-loader/babel" + ] +} +``` + +#### webpack.config.js ```js -render({ +export default { + entry: { + main: [ + 'react-hot-loader/patch', + 'webpack-hot-middleware/client?http://localhost:8080', + 'webpack/hot/only-dev-server', + './src/application.js' + ] + }, + plugins: [ + new webpack.HotModuleReplacementPlugin(), + ... + ], ... - routes: () => require('./routes') -}, common).then(({ rerender }) => { - if (module.hot) { - // This path must be equal to the path - // inside the `require()` call in the `routes` parameter - module.hot.accept('./routes', rerender) - } -}) +} ``` -## Additional `react-isomorphic-render.js` settings +## Other `react-isomorphic-render.js` settings ```javascript { @@ -732,11 +942,6 @@ render({ // User can add his own middleware to this `middleware` list reduxMiddleware: () => [...] - // (optional) - // Is called when Redux store has been created - // (can be used for setting up Webpack Hot Module Replacement) - onStoreCreated: ({ reloadReducer }) => {} - // (optional) // `http` utility settings http: @@ -808,11 +1013,6 @@ render({ header: 'Authorization' } - // (optional) - // `react-router`s `onUpdate` handler - // (is fired when a user performs navigation) - onNavigate: (location) => {} - // (optional) // `history` options (like `basename`) history: {} @@ -823,10 +1023,38 @@ render({ // restoring Redux state on the client-side. // (is `true` by default) parseDates: `true` / `false` + + // (optional) + // When supplying `event` instead of `events` + // as part of an asynchronous Redux action + // this will generate `events` from `event` + // using this function. + asynchronousActionEventNaming: event => ([ + `${event}_PENDING`, + `${event}_SUCCESS`, + `${event}_ERROR` + ]) + + // (optional) + // When using asynchronous action handlers + // this function will generate a Redux state property name for an event name. + // E.g. event `GET_USERS_ERROR` => state.`getUsersError`. + asynchronousActionHandlerStatePropertyNaming(event) { + // Converts `CAPS_LOCK_UNDERSCORED_NAMES` to `camelCasedNames` + return event.split('_') + .map((word, i) => { + let firstLetter = word.slice(0, 1) + if (i === 0) { + firstLetter = firstLetter.toLowerCase() + } + return firstLetter + word.slice(1).toLowerCase() + }) + .join('') + } } ``` -## Miscellaneous webpage rendering server options +## Other webpage rendering server options ```javascript { @@ -963,12 +1191,18 @@ render({ } ``` -## Miscellaneous client-side rendering options +## Other client-side rendering options ```javascript { ... + // (optional) + // `react-router`s `onUpdate` handler + // (is fired when a user performs navigation) + // (can be used for Google Analytics, for example) + onNavigate: (location) => {} + // (optional) // Enables/disables Redux development tools. // @@ -1016,17 +1250,17 @@ Client-side `render` function returns a `Promise` resolving to an object rerender // (Redux) rerender React application } ``` - + ## Gotchas This library is build system agnostic: you can use your favourite Grunt, Gulp, Browserify, RequireJS, Webpack, etc. diff --git a/index.common.js b/index.common.js index f2bdfb5..3c9ef06 100644 --- a/index.common.js +++ b/index.common.js @@ -28,9 +28,25 @@ exports.PRELOAD_METHOD_NAME = exports.Preload_method_name exports.Preload_options_name = preloading_middleware.Preload_options_name exports.PRELOAD_OPTIONS_NAME = exports.Preload_options_name -var redux_router = require('redux-router') +exports.action = require('./build/redux/asynchronous action handler').action +exports.create_handler = require('./build/redux/asynchronous action handler').create_handler +exports.createHandler = exports.create_handler +exports.state_connector = require('./build/redux/asynchronous action handler').state_connector +exports.stateConnector = exports.state_connector -exports.goto = redux_router.push -exports.redirect = redux_router.replace +exports.underscoredToCamelCase = require('./build/redux/naming').underscoredToCamelCase -exports.onEnter = require('./build/redux/on enter').default \ No newline at end of file +exports.event_name = require('./build/redux/naming').event_name +exports.eventName = exports.event_name + +// var redux_router = require('./build/redux/redux-router') + +// exports.goto = redux_router.push +// exports.redirect = redux_router.replace + +exports.goto = function(location) { return { type: '@@react-isomorphic-render/goto', location } } +exports.redirect = function(location) { return { type: '@@react-isomorphic-render/redirect', location } } + +exports.Link = require('./build/redux/link').default + +// exports.onEnter = require('./build/redux/on enter').default \ No newline at end of file diff --git a/index.es6.js b/index.es6.js index 55e39bb..abdf735 100644 --- a/index.es6.js +++ b/index.es6.js @@ -8,7 +8,7 @@ export const meta = webpage_meta // Redux -import { push, replace } from 'redux-router' +// import { push, replace } from './es6/redux/redux-router' import client from './es6/redux/client/client' import preload from './es6/redux/preload' @@ -30,7 +30,41 @@ export } from './es6/redux/middleware/preloading middleware' -export const goto = push -export const redirect = replace +export +{ + action, + create_handler, + create_handler as createHandler, + state_connector, + state_connector as stateConnector +} +from './es6/redux/asynchronous action handler' + +export +{ + default as asynchronous_action_handler, + default as asynchronousActionHandler +} +from './es6/redux/asynchronous action handler' + +export +{ + underscoredToCamelCase, + event_name, + event_name as eventName +} +from './es6/redux/naming' + +// export const goto = push +// export const redirect = replace + +export const goto = function(location) { return { type: '@@react-isomorphic-render/goto', location } } +export const redirect = function(location) { return { type: '@@react-isomorphic-render/redirect', location } } + +export +{ + default as Link +} +from './es6/redux/Link' -export { default as onEnter } from './es6/redux/on enter' \ No newline at end of file +// export { default as onEnter } from './es6/redux/on enter' \ No newline at end of file diff --git a/package.json b/package.json index 5d2cf02..861aeb2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-isomorphic-render", - "version": "8.0.13", + "version": "9.0.0", "description": "Isomorphic rendering with React, Redux, React-router and Redux-router. Includes support for Webpack", "main": "index.common.js", "module": "index.es6.js", @@ -14,13 +14,13 @@ "query-string": "^4.2.3", "react-helmet": "^3.2.3", "react-router-scroll": "^0.4.1", - "redux-router": "^2.1.2", "superagent": "^2.1.0", "uglify-js": "^2.7.5" }, "devDependencies": { "babel-cli": "^6.6.5", "babel-core": "^6.7.2", + "babel-plugin-transform-decorators-legacy": "^1.3.4", "babel-plugin-transform-react-display-name": "^6.5.0", "babel-plugin-transform-runtime": "^6.6.0", "babel-preset-es2015": "^6.6.0", @@ -44,7 +44,7 @@ "peerDependencies": { "react": ">= 0.14.0", "react-dom": ">= 0.14.0", - "react-router": ">= 2.3.0", + "react-router": "^2.3.0 || ^3.0.0", "history": ">= 2.0.0", "redux": "^3.0.0" }, @@ -52,7 +52,7 @@ "test": "mocha --compilers js:babel-core/register --colors --bail --reporter spec test/ --recursive", "test-coverage": "istanbul cover node_modules/mocha/bin/_mocha -- --compilers js:babel-core/register --colors --reporter dot test/ --recursive", "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --compilers js:babel-core/register --colors --reporter spec test/ --recursive", - "clean-for-build": "rimraf ./build/**/*", + "clean-for-build": "rimraf ./build/**/* ./es6/**/*", "build-commonjs-modules": "better-npm-run build-commonjs-modules", "build-es6-modules": "better-npm-run build-es6-modules", "build": "npm-run-all clean-for-build build-commonjs-modules build-es6-modules", @@ -79,6 +79,7 @@ "keywords": [ "react", "isomorphic", + "render", "redux" ], "author": "Halt Hammerzeit ", diff --git a/redux.js b/redux.js deleted file mode 100644 index d4883e9..0000000 --- a/redux.js +++ /dev/null @@ -1,24 +0,0 @@ -// For backwards compatibility until version 9.0.0 - -exports.preload = require('./build/redux/preload').default -exports.render = require('./build/redux/client/client').default - -var preloading_middleware = require('./build/redux/middleware/preloading middleware') - -exports.Preload_started = preloading_middleware.Preload_started -exports.PRELOAD_STARTED = exports.Preload_started -exports.Preload_finished = preloading_middleware.Preload_finished -exports.PRELOAD_FINISHED = exports.Preload_finished -exports.Preload_failed = preloading_middleware.Preload_failed -exports.PRELOAD_FAILED = exports.Preload_failed -exports.Preload_method_name = preloading_middleware.Preload_method_name -exports.PRELOAD_METHOD_NAME = exports.Preload_method_name -exports.Preload_options_name = preloading_middleware.Preload_options_name -exports.PRELOAD_OPTIONS_NAME = exports.Preload_options_name - -var redux_router = require('redux-router') - -exports.goto = redux_router.push -exports.redirect = redux_router.replace - -exports.onEnter = require('./build/redux/on enter').default \ No newline at end of file diff --git a/server.js b/server.js index 341c911..24b80e6 100644 --- a/server.js +++ b/server.js @@ -6,4 +6,7 @@ exports = module.exports = web_server exports.render = require('./build/page-server/render').default +exports.create_store = require('./build/redux/server/store').default +exports.createStore = exports.create_store + exports['default'] = web_server \ No newline at end of file diff --git a/source/page-server/render.js b/source/page-server/render.js index badb3a8..920ca96 100644 --- a/source/page-server/render.js +++ b/source/page-server/render.js @@ -5,36 +5,29 @@ import React from 'react' import ReactDOM from 'react-dom/server' import Html from './html' -import Http_client from '../http client' - -import redux_render from '../redux/server/render' +import redux_render from '../redux/server/render' import { render_on_server as react_router_render } from '../react-router/render' - -import create_store from '../redux/server/store' -import set_up_http_client from '../redux/http client' - -import { normalize_common_options } from '../redux/normalize' - +import create_store from '../redux/server/create store' +import create_http_client from '../redux/server/create http client' +import normalize_common_settings from '../redux/normalize' import timer from '../timer' // isomorphic (universal) rendering (middleware). // will be used in web_application.use(...) -export default async function({ initialize, localize, assets, application, request, render, loading, html, authentication, cookies }, common) +export default async function(common, { initialize, localize, assets, application, request, render, loading, html, authentication, cookies }) { // Trims a question mark in the end (just in case) const url = request.url.replace(/\?$/, '') + common = normalize_common_settings(common) + const { - reducer, - redux_middleware, - on_store_created, - promise_event_naming, routes, wrapper, parse_dates } - = normalize_common_options(common) + = normalize_common_settings(common) const error_handler = common.preload && common.preload.catch @@ -45,25 +38,18 @@ export default async function({ initialize, localize, assets, application, reque authentication_token = cookies.get(authentication.cookie) } - // Isomorphic http client (with cookie support) - const http_client = new Http_client - ({ - host : application ? application.host : undefined, - port : application ? application.port : undefined, - secure : application ? application.secure : false, - clone_request : request, - format_url : common.http && common.http.url, - parse_dates, - authentication_token, - authentication_token_header: authentication ? authentication.header : undefined - }) - - // initial store data (if using Redux) + // Create Redux store + + // Create HTTP client (Redux action creator `http` utility) + const http_client = create_http_client(common, authentication_token, application, request) + + // Initial store data let store_data = {} + // Time to fetch initial store data let initialize_time = 0 - // supports custom preloading before the page is rendered + // Supports custom preloading before the page is rendered // (for example to authenticate the user and retrieve user selected language) if (initialize) { @@ -72,34 +58,8 @@ export default async function({ initialize, localize, assets, application, reque initialize_time = initialize_timer() } - let store - - // create Redux store - if (reducer) - { - store = create_store(reducer, - { - server: true, - routes, - data: store_data, - middleware: redux_middleware, - on_store_created, - promise_event_naming, - on_preload_error : common.preload && common.preload.catch, - http_client, - preload_helpers : common.preload && common.preload.helpers, - on_navigate : common.on_navigate, - history_options : common.history - }) - } - - // Customization of `http` utility - // which can be used inside Redux action creators - set_up_http_client(http_client, - { - store, - on_before_send : common.http && common.http.request - }) + // Create Redux store + const store = create_store(common, store_data, http_client) // If `html` is not set then don't throw an error html = html || {} @@ -117,12 +77,13 @@ export default async function({ initialize, localize, assets, application, reque const store_parameter = { store } - // Normalize + // Normalize `html` parameters head = normalize_markup(typeof head === 'function' ? head (url, store_parameter) : head) body_start = normalize_markup(typeof body_start === 'function' ? body_start(url, store_parameter) : body_start) body_end = normalize_markup(typeof body_end === 'function' ? body_end (url, store_parameter) : body_end) - // Normalize + // Normalize assets + assets = typeof assets === 'function' ? assets(url, store_parameter) : assets if (assets.styles) diff --git a/source/page-server/web server.js b/source/page-server/web server.js index 0601c0b..34f52fa 100644 --- a/source/page-server/web server.js +++ b/source/page-server/web server.js @@ -6,7 +6,7 @@ import render_stack_trace from './html stack trace' import timer from '../timer' -export default function start_webpage_rendering_server(options, common) +export default function start_webpage_rendering_server(common, options) { const { @@ -91,8 +91,8 @@ export default function start_webpage_rendering_server(options, common) const total_timer = timer() - const { status, content, redirect, route, time } = await render_page - ({ + const { status, content, redirect, route, time } = await render_page(common, + { application, assets, initialize, @@ -108,8 +108,7 @@ export default function start_webpage_rendering_server(options, common) // Cookies for authentication token retrieval cookies: ctx.cookies - }, - common) + }) if (redirect) { diff --git a/source/react-router/client.js b/source/react-router/client.js index 7c8e192..92c0378 100644 --- a/source/react-router/client.js +++ b/source/react-router/client.js @@ -6,7 +6,7 @@ import { exists } from '../helpers' import localize_and_render from '../client' -import { normalize_common_options } from './normalize' +import normalize_common_settings from './normalize' // Performs client-side rendering // along with varios stuff like loading localized messages. @@ -18,7 +18,7 @@ import { normalize_common_options } from './normalize' // export default function render({ translation }, common) { - common = normalize_common_options(common) + common = normalize_common_settings(common) return localize_and_render ({ diff --git a/source/redux/Link.js b/source/redux/Link.js new file mode 100644 index 0000000..95e9476 --- /dev/null +++ b/source/redux/Link.js @@ -0,0 +1,103 @@ +// https://github.com/ReactTraining/react-router/blob/master/modules/Link.js + +import React, { Component, PropTypes } from 'react' +import { Link } from 'react-router' + +export default class Hyperlink extends Component +{ + static propTypes = + { + onClick : PropTypes.func, + target : PropTypes.string, + to : PropTypes.oneOfType([ PropTypes.string, PropTypes.object, PropTypes.func ]), + children : PropTypes.node + } + + static contextTypes = + { + router : PropTypes.object.isRequired, + store : PropTypes.object.isRequired + } + + constructor() + { + super() + + this.on_click = this.on_click.bind(this) + } + + on_click(event) + { + const { onClick, target, to } = this.props + const { router, store } = this.context + + if (!router) + { + throw new Error('s rendered outside of a router context cannot navigate.') + } + + if (!store) + { + throw new Error('s rendered outside of a Redux context cannot navigate.') + } + + if (onClick) + { + onClick(event) + } + + if (event.defaultPrevented) + { + return + } + + event.preventDefault() + + if (isModifiedEvent(event) || !isLeftClickEvent(event)) + { + return + } + + // If target prop is set (e.g. to "_blank"), let browser handle link. + if (target) + { + return + } + + const location = resolveToLocation(to, router) + + // // Just perform a javascript redirect if a `location` is an absolute URL + // if (typeof location === 'string') + // { + // if (location.indexOf('//') === 0 || location.indexOf('://') !== -1) + // { + // return document.location = location + // } + // } + + store.dispatch({ type: '@@react-isomorphic-render/preload', location }) + // .then(() => router.push(location)) + } + + render() + { + return { this.props.children } + } +} + +// export default withRouter(Hyperlink) + +function isLeftClickEvent(event) +{ + return event.button === 0 +} + +function isModifiedEvent(event) +{ + return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) +} + +function resolveToLocation(to, router) +{ + return typeof to === 'function' ? to(router.location) : to +} \ No newline at end of file diff --git a/source/redux/asynchronous action handler.js b/source/redux/asynchronous action handler.js new file mode 100644 index 0000000..ee51100 --- /dev/null +++ b/source/redux/asynchronous action handler.js @@ -0,0 +1,207 @@ +import { event_name } from './naming' +import normalize_common_settings from './normalize' + +// Returns Redux action creator +export function action({ namespace, event, promise, result }, handler) +{ + // Add handlers for: + // + // * pending + // * success + // * error + // + create_redux_handlers(handler, namespace, event, result) + + return function action_creator(...parameters) + { + const action = + { + event: event_name(namespace, event), + promise: http => promise.apply(this, parameters.concat(http)) + } + + return action + } +} + +// Creates Redux handler object +// (which will eventually be transformed into a reducer) +export function create_handler(settings) +{ + settings = normalize_common_settings(settings, { full: false }) + + const handlers = {} + const registered_state_properties = [] + + const result = + { + settings, + + handle(event, handler) + { + handlers[event] = handler + }, + + reducer(initial_state = {}) + { + // applies a handler based on the action type + // (is copy & paste'd for all action response handlers) + return function(state = initial_state, action_data = {}) + { + const handler = handlers[action_data.type] + + if (!handler) + { + return state + } + + let handler_argument = action_data + + if (action_data.result !== undefined) + { + handler_argument = action_data.result + } + else if (action_data.error !== undefined) + { + handler_argument = action_data.error + } + else if (Object.keys(action_data) === 1) + { + handler_argument = {} + } + + return handler(state, handler_argument) + } + }, + + registered_state_properties, + + add_state_properties() + { + registered_state_properties.push.apply(registered_state_properties, arguments) + } + } + + result.addStateProperties = result.add_state_properties + + return result +} + +// Adds handlers for: +// +// * pending +// * done +// * failed +// * reset error +// +function create_redux_handlers(handler, namespace, event, on_result) +{ + if (!handler.settings.asynchronous_action_event_naming) + { + throw new Error("`asynchronousActionEventNaming` function parameter was not passed") + } + + if (!handler.settings.asynchronous_action_handler_state_property_naming) + { + throw new Error("`asynchronousActionHandlerStatePropertyNaming` function parameter was not passed") + } + + const + [ + pending_event_name, + success_event_name, + error_event_name + ] + = handler.settings.asynchronous_action_event_naming(event) + + const pending_property_name = handler.settings.asynchronous_action_handler_state_property_naming(pending_event_name) + const error_property_name = handler.settings.asynchronous_action_handler_state_property_naming(error_event_name) + + // This info will be used in `storeConnector` + handler.add_state_properties(pending_property_name, error_property_name) + + // If `on_result` is a property name, + // then just set that property to the value of `result`. + if (typeof on_result === 'string') + { + handler.add_state_properties(on_result) + } + + // When Promise is created, + // clear `error`, + // set `pending` flag. + handler.handle(event_name(namespace, pending_event_name), (state, result) => + ({ + ...state, + // Set `pending` flag + [pending_property_name] : true, + // Clear `error` + [error_property_name] : undefined + })) + + // When Promise succeeds + handler.handle(event_name(namespace, success_event_name), (state, result) => + { + // This will be the new Redux state + let new_state + + // If `on_result` is a reducer, then call it, + // and the returned object will be the new state. + if (typeof on_result === 'function') + { + new_state = on_result(state, result) + + // If the reducer function didn't return + // the new state (which it should have done), + // then create the new state manually. + // (because `pending` property will be set later) + if (new_state === state) + { + new_state = { ...state } + } + } + // Else `on_result` is a property name, so populate it. + else + { + new_state = { ...state } + + // If `on_result` is a property name, + // then just set that property to the value of `result`. + if (typeof on_result === 'string') + { + new_state[on_result] = result + } + } + + // Clear `pending` flag + new_state[pending_property_name] = false + + // Return the new Redux state + return new_state + }) + + // When Promise fails, clear `pending` flag and set `error`. + // Can also clear `error` when no `error` is passed as part of an action. + handler.handle(event_name(namespace, error_event_name), (state, error) => + ({ + ...state, + [pending_property_name] : false, + [error_property_name] : error + })) +} + +// A little helper for Redux `@connect()` +export function state_connector(handler) +{ + return function connect_state(state) + { + const result = {} + + for (let property_name of handler.registered_state_properties) + { + result[property_name] = state[property_name] + } + + return result + } +} \ No newline at end of file diff --git a/source/redux/client/client.js b/source/redux/client/client.js index 1a57fde..ea644f3 100644 --- a/source/redux/client/client.js +++ b/source/redux/client/client.js @@ -1,70 +1,44 @@ import React from 'react' import ReactDOM from 'react-dom' -import Http_client from '../../http client' -import localize_and_render, { authentication_token as get_authentication_token } from '../../client' - +import localize_and_render from '../../client' +import { authentication_token as get_authentication_token } from '../../client' import render_on_client from './render' -import create_store from './store' -import { normalize_common_options } from '../normalize' -import set_up_http_client from '../http client' +import create_store from './create store' +import create_http_client from './create http client' +import normalize_common_settings from '../normalize' // Performs client-side rendering // along with varios stuff like loading localized messages. // // This function is what's gonna be called from the project's code on the client-side. // -export default function render({ devtools, translation }, common) +export default function render(common, specific = {}) { - common = normalize_common_options(common) + common = normalize_common_settings(common) + + const { devtools, translation } = specific + + // camelCase aliasing + const on_navigate = specific.on_navigate || specific.onNavigate // Read authentication token from a global variable // and also erase that global variable - let authentication_token = get_authentication_token() - - // `http` utility can be used inside Redux action creators - const http_client = new Http_client - ({ - format_url : common.http && common.http.url, - parse_dates : common.parse_dates, - authentication_token, - authentication_token_header: common.authentication ? common.authentication.header : undefined - }) + const authentication_token = get_authentication_token() - // Erase the local variable too - authentication_token = undefined - - // create ("rehydrate") Redux store - const store = create_store(common.reducer, - { - devtools, - middleware : common.redux_middleware, - on_store_created : common.on_store_created, - promise_event_naming : common.promise_event_naming, - on_preload_error : common.preload && common.preload.catch, - preload_helpers : common.preload && common.preload.helpers, - routes : common.routes, - on_navigate : common.on_navigate, - history_options : common.history, - data : window._store_data, - http_client - }) + // Create HTTP client (Redux action creator `http` utility) + const http_client = create_http_client(common, authentication_token) + // Create Redux store + const store = create_store(common, window._store_data, http_client, devtools) delete window._store_data - // Customization of `http` utility - // which can be used inside Redux action creators - set_up_http_client(http_client, - { - store, - on_before_send : common.http && common.http.request - }) - + // Render page return localize_and_render ({ translation, wrapper: common.wrapper, render_on_client, - render_parameters: { devtools, routes: common.routes, store } + render_parameters: { devtools, routes: common.routes, store, on_navigate } }) } \ No newline at end of file diff --git a/source/redux/client/create http client.js b/source/redux/client/create http client.js new file mode 100644 index 0000000..082fa35 --- /dev/null +++ b/source/redux/client/create http client.js @@ -0,0 +1,13 @@ +import Http_client from '../../http client' + +// `http` utility can be used inside Redux action creators +export default function create_http_client(common, authentication_token) +{ + return new Http_client + ({ + format_url : common.http && common.http.url, + parse_dates : common.parse_dates, + authentication_token, + authentication_token_header: common.authentication ? common.authentication.header : undefined + }) +} \ No newline at end of file diff --git a/source/redux/client/create store.js b/source/redux/client/create store.js new file mode 100644 index 0000000..b3bc028 --- /dev/null +++ b/source/redux/client/create store.js @@ -0,0 +1,32 @@ +import Http_client from '../../http client' +import set_up_http_client from '../http client' +import _create_store from './store' + +export default function create_store(common, data, http_client, devtools) +{ + // create ("rehydrate") Redux store + const store = _create_store(common.reducer, + { + devtools, + middleware : common.redux_middleware, + on_store_created : common.on_store_created, + asynchronous_action_event_naming : common.asynchronous_action_event_naming, + on_preload_error : common.preload && common.preload.catch, + preload_helpers : common.preload && common.preload.helpers, + routes : common.routes, + on_navigate : common.on_navigate, + history_options : common.history, + data, + http_client + }) + + // Customization of `http` utility + // which can be used inside Redux action creators + set_up_http_client(http_client, + { + store, + on_before_send : common.http && common.http.request + }) + + return store +} \ No newline at end of file diff --git a/source/redux/client/render.js b/source/redux/client/render.js index 3092743..c5c455f 100644 --- a/source/redux/client/render.js +++ b/source/redux/client/render.js @@ -1,20 +1,7 @@ -// This is a temporary workaround for fixing `redux-router` -// to work with `react-router@3`. -// The maintainers of `redux-router` don't want to merge my pull requests, -// so I'm making this workaround here. -// https://github.com/acdlite/redux-router/pull/282 -const createRouterObject = require('react-router/lib/RouterUtils').createRouterObject -require('react-router/lib/RouterUtils').createRouterObject = function(history, transitionManager, state = {}) -{ - return createRouterObject(history, transitionManager, state) -} - -const ReduxRouter = require('redux-router').ReduxRouter -const replace = require('redux-router').replace - import React from 'react' -// import { ReduxRouter, replace } from 'redux-router' -import { RouterContext, applyRouterMiddleware, match } from 'react-router' +// import { ReduxRouter, replace } from '../redux-router' +// import { RouterContext, applyRouterMiddleware, match } from 'react-router' +import { Router, applyRouterMiddleware, match } from 'react-router' import { useScroll } from 'react-router-scroll' import react_render_on_client from '../../render on client' @@ -25,7 +12,7 @@ import { location_url } from '../../location' // // Returns a Promise resolving to the rendered React component. // -export default function render_on_client({ devtools, create_page_element, routes, store, to }) +export default function render_on_client({ devtools, create_page_element, routes, store, to, on_navigate }) { // In short, Redux-router performs react-router routing asynchronously // which allows preloading pages before showing them. @@ -64,7 +51,7 @@ export default function render_on_client({ devtools, create_page_element, routes ({ history: store.history, routes: typeof routes === 'function' ? routes(store) : routes, - transition_manager: store.transitionManager + // transition_manager: store.transitionManager }) .then(({ redirect, router_props }) => { @@ -73,11 +60,24 @@ export default function render_on_client({ devtools, create_page_element, routes // then redirect to another url if (redirect) { - store.dispatch(replace(location_url(redirect))) - return + return store.dispatch + ({ + type: '@@react-isomorphic-render/redirect', + location: redirect + }) } - const router_element = + // const router_element = + + const onUpdate = function() + { + if (on_navigate) + { + on_navigate(this.state.location) + } + } + + const router_element = // Wraps with arbitrary React components (e.g. Redux ), // loads internationalization messages, @@ -149,54 +149,54 @@ export default function render_on_client({ devtools, create_page_element, routes // to use the supplied `transitionManager` instead of creating a new one. // https://github.com/reactjs/react-router/blob/master/modules/match.js // -function match_react_router({ history, routes, transition_manager }) +function match_react_router({ history, routes }) { return new Promise((resolve, reject) => { - // Get `location` from `history` - let location - const unlisten = history.listen(historyLocation => location = historyLocation) - - // Support history 3.x - if(history.getCurrentLocation) - { - location = history.getCurrentLocation() - } - - // Match `location` to a route (``s) - transition_manager.match(location, (error, redirect_location, next_router_state) => - { - if (error) - { - return reject(error) - } - - if (redirect_location) - { - return resolve({ redirect: redirect_location }) - } - - resolve({ router_props: next_router_state }) + // // Get `location` from `history` + // let location + // const unlisten = history.listen(historyLocation => location = historyLocation) - // Defer removing the listener to here to prevent DOM histories from having - // to unwind DOM event listeners unnecessarily, in case callback renders a - // and attaches another history listener. - unlisten() - }) + // // Support history 3.x + // if(history.getCurrentLocation) + // { + // location = history.getCurrentLocation() + // } - // match({ history, routes }, (error, redirect_location, router_props) => + // // Match `location` to a route (``s) + // transition_manager.match(location, (error, redirect_location, next_router_state) => // { // if (error) // { // return reject(error) // } - // + // if (redirect_location) // { // return resolve({ redirect: redirect_location }) // } - // - // return resolve({ router_props }) + + // resolve({ router_props: next_router_state }) + + // // Defer removing the listener to here to prevent DOM histories from having + // // to unwind DOM event listeners unnecessarily, in case callback renders a + // // and attaches another history listener. + // unlisten() // }) + + match({ history, routes }, (error, redirect_location, router_props) => + { + if (error) + { + return reject(error) + } + + if (redirect_location) + { + return resolve({ redirect: redirect_location }) + } + + return resolve({ router_props }) + }) }) } \ No newline at end of file diff --git a/source/redux/client/store.js b/source/redux/client/store.js index ab565e4..60ab7fe 100644 --- a/source/redux/client/store.js +++ b/source/redux/client/store.js @@ -1,8 +1,9 @@ -import { reduxReactRouter } from 'redux-router' +// import { reduxReactRouter } from '../redux-router' import createHistory from 'history/lib/createBrowserHistory' import create_store from '../store' export default function create_store_on_client(reducer, settings) { - return create_store(reduxReactRouter, createHistory, reducer, settings) + // reduxReactRouter, + return create_store(createHistory, reducer, settings) } \ No newline at end of file diff --git a/source/redux/middleware/asynchronous middleware.js b/source/redux/middleware/asynchronous middleware.js index f0fec4c..aadd414 100644 --- a/source/redux/middleware/asynchronous middleware.js +++ b/source/redux/middleware/asynchronous middleware.js @@ -12,7 +12,7 @@ import { exists, is_object } from '../../helpers' // of `asynchronous_middleware` won't send actions to user-supplied middleware, // therefore there's an additional `dispatch_event` argument // which is a function to hack around that limitation. -export default function asynchronous_middleware(http_client, dispatch_event, { promise_event_naming }) +export default function asynchronous_middleware(http_client, dispatch_event, { asynchronous_action_event_naming }) { return ({ dispatch, getState }) => { @@ -43,13 +43,13 @@ export default function asynchronous_middleware(http_client, dispatch_event, { p // generate the three event names automatically based on a base event name if (typeof event === 'string') { - events = promise_event_naming(event) + events = asynchronous_action_event_naming(event) } // sanity check if (!events || events.length !== 3) { - throw new Error(`"events" property must be an array of events: e.g. ['pending', 'success', 'error']`) + throw new Error(`"events" property must be an array of 3 event names: e.g. ['pending', 'success', 'error']`) } // event names diff --git a/source/redux/middleware/history middleware.js b/source/redux/middleware/history middleware.js new file mode 100644 index 0000000..d9eb12d --- /dev/null +++ b/source/redux/middleware/history middleware.js @@ -0,0 +1,22 @@ +export default function history_middleware(history) +{ + return ({ getState, dispatch }) => + { + return next => action => + { + if (action.type === '@@react-isomorphic-render/redirect') + { + dispatch({ type: '@@react-isomorphic-render/navigated', location: action.location }) + return history.replace(action.location) + } + + if (action.type === '@@react-isomorphic-render/goto') + { + dispatch({ type: '@@react-isomorphic-render/navigated', location: action.location }) + return history.push(action.location) + } + + return next(action) + } + } +} \ No newline at end of file diff --git a/source/redux/middleware/on route update middleware.js b/source/redux/middleware/on route update middleware.js index 391aa18..be28c60 100644 --- a/source/redux/middleware/on route update middleware.js +++ b/source/redux/middleware/on route update middleware.js @@ -1,47 +1,50 @@ -import { ROUTER_DID_CHANGE } from 'redux-router/lib/constants' +// import { ROUTER_DID_CHANGE } from '../redux-router/constants' -import { locations_are_equal } from '../../location' +// import { locations_are_equal } from '../../location' -// Implements `react-router`s `onUpdate` handler -export default function on_route_update_middleware(navigation_performed) -{ - return ({ dispatch, getState }) => - { - return next => action => - { - // If it isn't a `redux-router` navigation event then do nothing - if (action.type !== ROUTER_DID_CHANGE) - { - // Do nothing - return next(action) - } +// // Implements `react-router`s `onUpdate` handler +// export default function on_route_update_middleware(navigation_performed, history) +// { +// return ({ dispatch, getState }) => +// { +// return next => action => +// { +// // If it isn't a `redux-router` navigation event then do nothing +// // if (action.type !== ROUTER_DID_CHANGE) +// if (action.type !== '@@react-isomorphic-render/navigated') +// { +// // Do nothing +// return next(action) +// } - // on the server side "getState().router" is - // either `undefined` (in case of no `@preload()`) - // or a Promise (in case of `@preload()`) - const is_server_side = !getState().router - || (getState().router && typeof getState().router.then === 'function') +// // // on the server side "getState().router" is +// // // either `undefined` (in case of no `@preload()`) +// // // or a Promise (in case of `@preload()`) +// // const is_server_side = !getState().router +// // || (getState().router && typeof getState().router.then === 'function') - // `onUpdate` is not supposed to be fired on the server side - // since all navigation is basically HTTP redirection. - if (is_server_side) - { - return next(action) - } +// // // `onUpdate` is not supposed to be fired on the server side +// // // since all navigation is basically HTTP redirection. +// // if (is_server_side) +// // { +// // return next(action) +// // } - // When routing is initialized on the client side - // then ROUTER_DID_CHANGE event will be fired, - // so ignore this initialization event. - if (locations_are_equal(action.payload.location, getState().router.location)) - { - // Ignore the event - return next(action) - } +// // // When routing is initialized on the client side +// // // then ROUTER_DID_CHANGE event will be fired, +// // // so ignore this initialization event. +// // // if (locations_are_equal(action.payload.location, getState().router.location)) +// // if (locations_are_equal(action.location, getState().router.location)) +// // { +// // // Ignore the event +// // return next(action) +// // } - // Fire `onUpdate` handler - navigation_performed(action.payload.location) - // Proceed as usual - next(action) - } - } -} \ No newline at end of file +// // Fire `onUpdate` handler +// // navigation_performed(action.payload.location) +// navigation_performed(action.location) +// // Proceed as usual +// next(action) +// } +// } +// } \ No newline at end of file diff --git a/source/redux/middleware/preloading middleware.js b/source/redux/middleware/preloading middleware.js index e6caa64..f0d52ef 100644 --- a/source/redux/middleware/preloading middleware.js +++ b/source/redux/middleware/preloading middleware.js @@ -6,13 +6,15 @@ // // in all the other cases it will do nothing -import { ROUTER_DID_CHANGE } from 'redux-router/lib/constants' -import { replace } from 'redux-router' +import { match } from 'react-router' + +// import { ROUTER_DID_CHANGE } from '../redux-router/constants' +// import { replace } from '../redux-router' import { location_url, locations_are_equal } from '../../location' -export const Preload_method_name = '__react_preload__' -export const Preload_options_name = '__react_preload_options__' +export const Preload_method_name = '__preload__' +export const Preload_options_name = '__preload_options__' export const Preload_started = '@@react-isomorphic-render/redux/preload started' export const Preload_finished = '@@react-isomorphic-render/redux/preload finished' @@ -60,16 +62,16 @@ const preloader = (server, components, getState, dispatch, location, parameters, // // (also, GET query parameters would also need to be compared in that case) // - if (!server) - { - let previous_route_components = getState().router.components + // if (!server) + // { + // let previous_route_components = getState().router.components - while (components.length > 1 && previous_route_components[0] === components[0]) - { - previous_route_components = previous_route_components.slice(1) - components = components.slice(1) - } - } + // while (components.length > 1 && previous_route_components[0] === components[0]) + // { + // previous_route_components = previous_route_components.slice(1) + // components = components.slice(1) + // } + // } // finds all `preload` (or `preload_deferred`) methods // (they will be executed in parallel) @@ -85,7 +87,7 @@ const preloader = (server, components, getState, dispatch, location, parameters, try { // `preload()` returns a Promise - const promise = component[Preload_method_name](preload_arguments) + let promise = component[Preload_method_name](preload_arguments) // Sanity check if (typeof promise.then !== 'function') @@ -93,6 +95,12 @@ const preloader = (server, components, getState, dispatch, location, parameters, return Promise.reject(`Preload function didn't return a Promise:`, preload) } + // Convert `array`s into `Promise.all(array)` + if (Array.isArray(promise)) + { + promise = Promise.all(promise) + } + return promise } catch (error) @@ -168,25 +176,31 @@ const preloader = (server, components, getState, dispatch, location, parameters, // won't send actions to that `reduxReactRouter` middleware, // therefore there's the third `dispatch_event` argument // which is a function to hack around that limitation. -export default function preloading_middleware(server, error_handler, dispatch_event, preload_helpers) +export default function preloading_middleware(server, error_handler, dispatch_event, preload_helpers, routes, history) { return ({ getState, dispatch }) => next => action => { - // If it isn't a `redux-router` navigation event then do nothing - if (action.type !== ROUTER_DID_CHANGE) + if (action.type !== '@@react-isomorphic-render/preload') { // Do nothing return next(action) } - // When routing is initialized on the client side - // then ROUTER_DID_CHANGE event will be fired, - // so ignore this initialization event. - if (!server && !getState().router) - { - // Ignore the event - return next(action) - } + // // If it isn't a `redux-router` navigation event then do nothing + // if (action.type !== ROUTER_DID_CHANGE) + // { + // // Do nothing + // return next(action) + // } + + // // When routing is initialized on the client side + // // then ROUTER_DID_CHANGE event will be fired, + // // so ignore this initialization event. + // if (!server && !getState().router) + // { + // // Ignore the event + // return next(action) + // } // Promise error handler const handle_error = error => @@ -237,132 +251,188 @@ export default function preloading_middleware(server, error_handler, dispatch_ev } } - // All these three properties are the next `react-router` state - // (taken from `history.listen(function(error, nextRouterState))`) - const { components, location, params } = action.payload - // Preload all the required data for this route (page) - const preload = preloader(server, components, getState, dispatch_event, location, params, preload_helpers) - // If nothing to preload, just move to the next middleware - if (!preload) + return match_promise(routes, { + dispatch: dispatch_event, + getState + }, history, action.location).then(({ redirect, router_state }) => { - return next(action) - } + if (redirect) + { + // Shouldn't happen in the current setup + if (server) + { + const error = new Error() + error._redirect = location_url(redirect) + throw error + } - // `window.__preloading_page` holds client side page preloading status. - // If there's preceeding navigation pending, then cancel that previous navigation. - if (!server && window.__preloading_page && !window.__preloading_page.cancelled) - { - // window.__preloading_page.promise.cancel() - window.__preloading_page.cancelled = true - } + return dispatch_event({ type: '@@react-isomorphic-render/redirect', location: redirect }) + } - // Page loading indicator could listen for this event - dispatch_event({ type: Preload_started }) + // All these three properties are the next `react-router` state + // (taken from `history.listen(function(error, nextRouterState))`) + // const { components, location, params } = action.payload + const { components, location, params } = router_state - // Holds the cancellation flag for this navigation process - const preloading = { cancelled: false } + // Preload all the required data for this route (page) + const preload = preloader(server, components, getState, dispatch_event, location, params, preload_helpers) - // This Promise is only used in server-side rendering. - // Client-side rendering never uses this Promise. - const promise = - // preload this route - preload() - // proceed with routing - .then(() => + // If nothing to preload, just move to the next middleware + if (!preload) { - // If this navigation process was cancelled - // before @preload() finished its work, - // then don't take any further steps on this cancelled navigation. - if (preloading.cancelled) + if (!server) { - return + return dispatch_event({ type: '@@react-isomorphic-render/goto', location }) } + return + } - // Page loading indicator could listen for this event - dispatch_event({ type: Preload_finished }) + // `window.__preloading_page` holds client side page preloading status. + // If there's preceeding navigation pending, then cancel that previous navigation. + if (!server && window.__preloading_page && !window.__preloading_page.cancelled) + { + // window.__preloading_page.promise.cancel() + window.__preloading_page.cancelled = true + } - // Pass ROUTER_DID_CHANGE to redux-router middleware - // so that react-router renders the new route. - next(action) - }) - .catch(error => + // Page loading indicator could listen for this event + dispatch_event({ type: Preload_started }) + + // Holds the cancellation flag for this navigation process + const preloading = { cancelled: false } + + // If on the client side, then store the current pending navigation, + // so that it can be cancelled when a new navigation process takes place + // before the current navigation process finishes. + if (!server) { - // If this navigation process was cancelled - // before @preload() finished its work, - // then don't take any further steps on this cancelled navigation. - if (preloading.cancelled) - { - return - } + // preloading.promise = promise + window.__preloading_page = preloading + } - // If the error was a redirection exception (not a error), - // then just exit and do nothing. - // (happens only on server side or when using `onEnter` helper) - if (error._redirect) + // This Promise is only used in server-side rendering. + // Client-side rendering never uses this Promise. + + // preload this route + return preload() + // proceed with routing + .then(() => { + // If this navigation process was cancelled + // before @preload() finished its work, + // then don't take any further steps on this cancelled navigation. + if (preloading.cancelled) + { + return + } + + // Page loading indicator could listen for this event + dispatch_event({ type: Preload_finished }) + if (!server) { - // Page loading indicator could listen for this event - dispatch_event({ type: Preload_finished }) + dispatch_event({ type: '@@react-isomorphic-render/goto', location }) + } - return dispatch_event(replace(error._redirect)) + // // Pass ROUTER_DID_CHANGE to redux-router middleware + // // so that react-router renders the new route. + // next(action) + }) + .catch(error => + { + // If this navigation process was cancelled + // before @preload() finished its work, + // then don't take any further steps on this cancelled navigation. + if (preloading.cancelled) + { + return } - throw error - } + // If the error was a redirection exception (not a error), + // then just exit and do nothing. + // (happens only on server side or when using `onEnter` helper) + if (error._redirect) + { + if (!server) + { + // Page loading indicator could listen for this event + dispatch_event({ type: Preload_finished }) - // Reset the Promise temporarily placed into the router state - // by the code below - // (fixes "Invariant Violation: `mapStateToProps` must return an object. - // Instead received [object Promise]") - if (server) - { - getState().router = null - } + return dispatch_event({ type: '@@react-isomorphic-render/redirect', location: error._redirect }) - // Page loading indicator could listen for this event - dispatch_event({ type: Preload_failed, error }) + // return dispatch_event(replace(error._redirect)) + } - // Handle preloading error - // (either `redirect` to an "error" page - // or rethrow the error up the Promise chain) - handle_error(error) - }) + throw error + } - // If on the client side, then store the current pending navigation, - // so that it can be cancelled when a new navigation process takes place - // before the current navigation process finishes. - if (!server) - { - // preloading.promise = promise - window.__preloading_page = preloading - } + // // Reset the Promise temporarily placed into the router state + // // by the code below + // // (fixes "Invariant Violation: `mapStateToProps` must return an object. + // // Instead received [object Promise]") + // if (server) + // { + // getState().router = null + // } + + // Page loading indicator could listen for this event + dispatch_event({ type: Preload_failed, error }) + + // Handle preloading error + // (either `redirect` to an "error" page + // or rethrow the error up the Promise chain) + handle_error(error) + }) + }) + + // // On the server side + // if (server) + // { + // // `state.router` is null until `replaceRoutesMiddleware` is called + // // with the currently paused ROUTER_DID_CHANGE action as a parameter + // // in a subsequent middleware call, so until that next middleware is called + // // we can use this router state variable to store the promise + // // to let the server know when it can render the page. + // // + // // This variable will be instantly available + // // (and therefore .then()-nable) + // // in ./source/redux/render.js, + // // and when everything has finished preloading (asynchronously), + // // then the next middleware is called, + // // `replaceRoutesMiddleware` gets control, + // // and replaces `state.router` with the proper Redux-router router state. + // // + // // (the `promise` above could still resolve instantly, hence the `if` check) + // // + // if (!getState().router) + // { + // getState().router = promise + // } + // } + } +} - // On the server side - if (server) +function match_promise(routes, store, history, location) +{ + routes = typeof routes === 'function' ? routes(store) : routes + + return new Promise((resolve, reject) => + { + match({ history, routes, location }, (error, redirect_location, router_state) => { - // `state.router` is null until `replaceRoutesMiddleware` is called - // with the currently paused ROUTER_DID_CHANGE action as a parameter - // in a subsequent middleware call, so until that next middleware is called - // we can use this router state variable to store the promise - // to let the server know when it can render the page. - // - // This variable will be instantly available - // (and therefore .then()-nable) - // in ./source/redux/render.js, - // and when everything has finished preloading (asynchronously), - // then the next middleware is called, - // `replaceRoutesMiddleware` gets control, - // and replaces `state.router` with the proper Redux-router router state. - // - // (the `promise` above could still resolve instantly, hence the `if` check) - // - if (!getState().router) + if (error) { - getState().router = promise + return reject(error) } - } - } + + if (redirect_location) + { + return resolve({ redirect: redirect_location }) + } + + return resolve({ router_state }) + }) + }) } \ No newline at end of file diff --git a/source/redux/naming.js b/source/redux/naming.js new file mode 100644 index 0000000..49ceab9 --- /dev/null +++ b/source/redux/naming.js @@ -0,0 +1,33 @@ +// Converts `UNDERSCORED_NAMES` to `camelCasedNames` +export function underscoredToCamelCase(string) +{ + return string.split('_') + .map((word, i) => + { + let firstLetter = word.slice(0, 1) + const rest = word.slice(1) + + if (i === 0) + { + firstLetter = firstLetter.toLowerCase() + } + else + { + firstLetter = firstLetter.toUpperCase() + } + + return firstLetter + rest.toLowerCase() + }) + .join('') +} + +// Converts `namespace` and `event` into a namespaced event name +export function event_name(namespace, event) +{ + if (!namespace) + { + return event + } + + return `${namespace}: ${event}` +} \ No newline at end of file diff --git a/source/redux/normalize.js b/source/redux/normalize.js index 4f301b0..b418d1f 100644 --- a/source/redux/normalize.js +++ b/source/redux/normalize.js @@ -1,29 +1,44 @@ import { clone } from '../helpers' -export function normalize_common_options(common) +export default function normalize_common_settings(common, options = {}) { - if (!common) + if (common === undefined) { - throw new Error(`Common options weren't passed. Perhaps you've upgraded to react-isomorphic-render 4.0.0 in which case check the new API documentation.`) + throw new Error(`Common settings weren't passed.`) + } + + if (typeof common !== 'object') + { + throw new Error(`Expected a settings object, got ${typeof common}: ${common}`) } common = clone(common) - if (!common.routes) + if (options.full !== false) { - throw new Error(`"routes" parameter is required`) + if (!common.routes) + { + throw new Error(`"routes" parameter is required`) + } + + if (!common.reducer) + { + throw new Error(`"reducer" parameter is required`) + } } - if (!common.reducer) + // camelCase aliasing + if (common.asynchronousActionEventNaming) { - throw new Error(`"reducer" parameter is required`) + common.asynchronous_action_event_naming = common.asynchronousActionEventNaming + delete common.asynchronousActionEventNaming } // camelCase aliasing - if (common.onStoreCreated) + if (common.asynchronousActionHandlerStatePropertyNaming) { - common.on_store_created = common.onStoreCreated - delete common.onStoreCreated + common.asynchronous_action_handler_state_property_naming = common.asynchronousActionHandlerStatePropertyNaming + delete common.asynchronousActionHandlerStatePropertyNaming } // camelCase aliasing diff --git a/source/redux/on enter.js b/source/redux/on enter.js deleted file mode 100644 index 9051618..0000000 --- a/source/redux/on enter.js +++ /dev/null @@ -1,60 +0,0 @@ -import React, { Component } from 'react' -import hoist_statics from 'hoist-non-react-statics' -import { replace as redirect } from 'redux-router' - -import { Preload_method_name } from './middleware/preloading middleware' - -export default function onEnter(on_enter) -{ - return function(Wrapped) - { - class On_enter extends Component - { - render() - { - return - } - } - - On_enter.displayName = `onEnter(${get_display_name(Wrapped)})` - - hoist_statics(On_enter, Wrapped) - - const preloader = On_enter[Preload_method_name] - - On_enter[Preload_method_name] = function on_enter_then_preload(parameters) - { - let redirect_to - const redirect = to => redirect_to = to - - const proceed = () => preloader ? preloader(parameters) : Promise.resolve() - - const on_enter_result = on_enter(parameters, redirect) - - // If it's not a Promise, then just proceed - if (!on_enter_result.then) - { - return proceed() - } - - return on_enter_result.then(() => - { - if (redirect_to) - { - const error = new Error(`Redirecting to ${redirect_to} (this is not an error)`) - error._redirect = redirect_to - throw error - } - - return proceed() - }) - } - - return On_enter - } -} - -function get_display_name(Wrapped) -{ - return Wrapped.displayName || Wrapped.name || 'Component' -} \ No newline at end of file diff --git a/source/redux/redux-router/README.md b/source/redux/redux-router/README.md new file mode 100644 index 0000000..ae354f5 --- /dev/null +++ b/source/redux/redux-router/README.md @@ -0,0 +1,9 @@ +UPDATE: `redux-router` is currently not used in this library, but is kept here just in case + +Since `redux-router` maintainers are incompetent and lazy, they don't want to merge my Pull Requests, I'm forking `redux-router` repo here. + +The changes made: + + * https://github.com/acdlite/redux-router/pull/282 + * https://github.com/acdlite/redux-router/pull/272 + * `routeReplacement` functionality removed: this way `react-router` won't re-match on saving React components, therefore `@preload()` won't be called every time a developer edits a React component. A theoretical implication of this would be that when editing `./routes.js` the current URL won't re-match, as already said. But editing `routes` is such a seldom activity that one may assume a developer almost never edits `routes`. \ No newline at end of file diff --git a/source/redux/redux-router/ReduxRouter.js b/source/redux/redux-router/ReduxRouter.js new file mode 100644 index 0000000..1b4d71a --- /dev/null +++ b/source/redux/redux-router/ReduxRouter.js @@ -0,0 +1,111 @@ +import React, { Component, PropTypes } from 'react'; +import { connect } from 'react-redux'; +import { RouterContext as DefaultRoutingContext } from 'react-router'; +import { createRouterObject } from 'react-router/lib/RouterUtils'; +import routerStateEquals from './routerStateEquals'; +import { ROUTER_STATE_SELECTOR } from './constants'; +import { initRoutes } from './actionCreators'; +// import { initRoutes, replaceRoutes } from './actionCreators'; + +function memoizeRouterStateSelector(selector) { + let previousRouterState = null; + + return state => { + const nextRouterState = selector(state); + if (routerStateEquals(previousRouterState, nextRouterState)) { + return previousRouterState; + } + previousRouterState = nextRouterState; + return nextRouterState; + }; +} + +function getRoutesFromProps(props) { + return props.routes || props.children; +} + +class ReduxRouter extends Component { + static propTypes = { + children: PropTypes.node + } + + static contextTypes = { + store: PropTypes.object + } + + constructor(props, context) { + super(props, context); + this.router = createRouterObject(context.store.history, context.store.transitionManager, {}); + } + + componentWillMount() { + this.context.store.dispatch(initRoutes(getRoutesFromProps(this.props))); + } + + // componentWillReceiveProps(nextProps) { + // this.receiveRoutes(getRoutesFromProps(nextProps)); + // } + + // receiveRoutes(routes) { + // if (!routes) return; + + // const { store } = this.context; + // store.dispatch(replaceRoutes(routes)); + // } + + render() { + const { store } = this.context; + + if (!store) { + throw new Error( + 'Redux store missing from context of . Make sure you\'re ' + + 'using a ' + ); + } + + const { + history, + [ROUTER_STATE_SELECTOR]: routerStateSelector + } = store; + + if (!history || !routerStateSelector) { + throw new Error( + 'Redux store not configured properly for . Make sure ' + + 'you\'re using the reduxReactRouter() store enhancer.' + ); + } + + return ( + + ); + } +} + +@connect( + (state, { routerStateSelector }) => routerStateSelector(state) || {} +) +class ReduxRouterContext extends Component { + static propTypes = { + location: PropTypes.object, + RoutingContext: PropTypes.func + } + + render() { + const { location } = this.props; + + if (location === null || location === undefined) { + return null; // Async matching + } + + const RoutingContext = this.props.RoutingContext || DefaultRoutingContext; + + return ; + } +} + +export default ReduxRouter; diff --git a/source/redux/redux-router/actionCreators.js b/source/redux/redux-router/actionCreators.js new file mode 100644 index 0000000..95b1814 --- /dev/null +++ b/source/redux/redux-router/actionCreators.js @@ -0,0 +1,63 @@ +import { ROUTER_DID_CHANGE, INIT_ROUTES, HISTORY_API } from './constants'; + +// REPLACE_ROUTES + +/** + * Action creator for signaling that the router has changed. + * @private + * @param {RouterState} state - New router state + * @return {Action} Action object + */ +export function routerDidChange(state) { + return { + type: ROUTER_DID_CHANGE, + payload: state + }; +} + +/** + * Action creator that initiates route config + * @private + * @param {Array|ReactElement} routes - New routes + */ +export function initRoutes(routes) { + return { + type: INIT_ROUTES, + payload: routes + }; +} + +/** + * Action creator that replaces the current route config + * @private + * @param {Array|ReactElement} routes - New routes + */ +// export function replaceRoutes(routes) { +// return { +// type: REPLACE_ROUTES, +// payload: routes +// }; +// } + +/** + * Creates an action creator for calling a history API method. + * @param {string} method - Name of method + * @returns {ActionCreator} Action creator with same parameters as corresponding + * history method + */ +export function historyAPI(method) { + return (...args) => ({ + type: HISTORY_API, + payload: { + method, + args + } + }); +} + +export const push = historyAPI('push'); +export const replace = historyAPI('replace'); +export const setState = historyAPI('setState'); +export const go = historyAPI('go'); +export const goBack = historyAPI('goBack'); +export const goForward = historyAPI('goForward'); diff --git a/source/redux/redux-router/addHistorySynchronization.js b/source/redux/redux-router/addHistorySynchronization.js new file mode 100644 index 0000000..6474b18 --- /dev/null +++ b/source/redux/redux-router/addHistorySynchronization.js @@ -0,0 +1,72 @@ +import { routerDidChange } from './actionCreators'; +import routerStateEquals from './routerStateEquals'; + +// A function taking `options` and returning a Redux store enhancer +// which detects `react-router` transitions (hyperlink clicks) +// and manual `react-router` navigation (like `dispatch(replace(location))`) +export default function historySynchronization(next) { + return options => { + return createStore => (reducer, initialState) => { + // No middlewares are added to the store + const store = next(options)(createStore)(reducer, initialState); + + const { history, transitionManager } = store; + + let prevRouterState; + let routerState; + + const { + onError, + routerStateSelector + } = options; + + // On each `react-router` event (e.g. navigation) + // checks if the URL has actually changed, + // and if it did, then it dispatches a `ROUTER_DID_CHANGE` Redux event. + // + // This `.listen()` call also triggers `react-router` `match()` + // (which means that `react-router` routes are matched and `onEnter`ed) + // after which this listener is called, and, therefore, + // `ROUTER_DID_CHANGE` is dispatched. + // + transitionManager.listen((error, nextRouterState) => { + if (error) { + onError(error); + return; + } + + // If a navigation event took place, + // then dispatch a `ROUTER_DID_CHANGE` Redux event. + if (!routerStateEquals(routerState, nextRouterState)) { + prevRouterState = routerState; + routerState = nextRouterState; + store.dispatch(routerDidChange(nextRouterState)); + } + }); + + // On each Redux state update, + // if `state.router` was manually changed + // (e.g. due to a `dispatch(replace(location))` action), + // then perform the actual URL navigation + // (including `react-router` transition + // which is then gonna be caught in the `transitionManager` listener) + store.subscribe(() => { + const nextRouterState = routerStateSelector(store.getState()); + + // If `state.router` was manually changed + if ( + nextRouterState && + prevRouterState !== nextRouterState && + !routerStateEquals(routerState, nextRouterState) + ) { + routerState = nextRouterState; + const { state, pathname, query } = nextRouterState.location; + // Then perform the actual URL navigation + history.replace({state, pathname, query}); + } + }); + + return store; + }; + } +} \ No newline at end of file diff --git a/source/redux/redux-router/client.js b/source/redux/redux-router/client.js new file mode 100644 index 0000000..71852c6 --- /dev/null +++ b/source/redux/redux-router/client.js @@ -0,0 +1,25 @@ +// This is the `reduxReactRouter` middleware: +// import { reduxReactRouter } from 'redux-router' +// applyMiddleware(reduxReactRouter) + +import { compose } from 'redux'; +import reduxReactRouter from './reduxReactRouter'; +import useDefaults from './useDefaults'; +import addHistorySynchronization from './addHistorySynchronization'; +// import routeReplacement from './routeReplacement'; + +// Returns a function taking `options` +// and returning a Redux store enhancer +// (which is a composite of 3 store enhancers) +// +// This piece of code is very complex and very advanced. +// +// Redux store enhancers are a very advanced and complex and non-trivial stuff +// https://github.com/reactjs/redux/blob/master/src/applyMiddleware.js +// http://redux.js.org/docs/advanced/Middleware.html +// +export default function reduxReactRouterMiddleware(options) { + options = useDefaults(options); + // return addHistorySynchronization(routeReplacement(reduxReactRouter))(options); + return addHistorySynchronization(reduxReactRouter)(options); +} diff --git a/source/redux/redux-router/constants.js b/source/redux/redux-router/constants.js new file mode 100644 index 0000000..eb7b1ae --- /dev/null +++ b/source/redux/redux-router/constants.js @@ -0,0 +1,13 @@ +// Signals that the router's state has changed. It should +// never be called by the application, only as an implementation detail of +// redux-react-router. +export const ROUTER_DID_CHANGE = '@@reduxReactRouter/routerDidChange'; + +export const HISTORY_API = '@@reduxReactRouter/historyAPI'; +export const MATCH = '@@reduxReactRouter/match'; +export const INIT_ROUTES = '@@reduxReactRouter/initRoutes'; +// export const REPLACE_ROUTES = '@@reduxReactRouter/replaceRoutes'; + +export const ROUTER_STATE_SELECTOR = '@@reduxReactRouter/routerStateSelector'; + +// export const DOES_NEED_REFRESH = '@@reduxReactRouter/doesNeedRefresh'; diff --git a/source/redux/redux-router/historyMiddleware.js b/source/redux/redux-router/historyMiddleware.js new file mode 100644 index 0000000..499ca61 --- /dev/null +++ b/source/redux/redux-router/historyMiddleware.js @@ -0,0 +1,15 @@ +import { HISTORY_API } from './constants'; + +/** + * Middleware for interacting with the history API + * @param {History} History object + */ +export default function historyMiddleware(history) { + return () => next => action => { + if (action.type === HISTORY_API) { + const { method, args } = action.payload; + return history[method](...args); + } + return next(action); + }; +} diff --git a/source/redux/redux-router/index.js b/source/redux/redux-router/index.js new file mode 100644 index 0000000..2ef8544 --- /dev/null +++ b/source/redux/redux-router/index.js @@ -0,0 +1,14 @@ +export routerStateReducer from './routerStateReducer'; +export ReduxRouter from './ReduxRouter'; +export reduxReactRouter from './client'; +export isActive from './isActive'; + +export { + historyAPI, + push, + replace, + setState, + go, + goBack, + goForward +} from './actionCreators'; diff --git a/source/redux/redux-router/isActive.js b/source/redux/redux-router/isActive.js new file mode 100644 index 0000000..91324d9 --- /dev/null +++ b/source/redux/redux-router/isActive.js @@ -0,0 +1,17 @@ +import _isActive from 'react-router/lib/isActive'; + +/** + * Creates a router state selector that returns whether or not the given + * pathname and query are active. + * @param {String} pathname + * @param {Object} query + * @param {Boolean} indexOnly + * @return {Boolean} + */ +export default function isActive(pathname, query, indexOnly = false) { + return state => { + if (!state) return false; + const { location, params, routes } = state; + return _isActive({ pathname, query }, indexOnly, location, routes, params); + }; +} diff --git a/source/redux/redux-router/matchMiddleware.js b/source/redux/redux-router/matchMiddleware.js new file mode 100644 index 0000000..8d7e1ee --- /dev/null +++ b/source/redux/redux-router/matchMiddleware.js @@ -0,0 +1,17 @@ +import { routerDidChange } from './actionCreators'; +import { MATCH } from './constants'; + +export default function matchMiddleware(match) { + return ({ dispatch }) => next => action => { + if (action.type === MATCH) { + const { url, callback } = action.payload; + match(url, (error, redirectLocation, routerState) => { + if (!error && !redirectLocation && routerState) { + dispatch(routerDidChange(routerState)); + } + callback(error, redirectLocation, routerState); + }); + } + return next(action); + }; +} diff --git a/source/redux/redux-router/reduxReactRouter.js b/source/redux/redux-router/reduxReactRouter.js new file mode 100644 index 0000000..d234585 --- /dev/null +++ b/source/redux/redux-router/reduxReactRouter.js @@ -0,0 +1,63 @@ +import { applyMiddleware } from 'redux'; +import { useRouterHistory, createRoutes } from 'react-router'; +import createTransitionManager from 'react-router/lib/createTransitionManager' ; +import historyMiddleware from './historyMiddleware'; +import { ROUTER_STATE_SELECTOR } from './constants'; + +// A function taking `options` and returning a Redux store enhancer +// which adds `historyMiddleware` and also creates a `history` and a `transitionManager`. +export default function reduxReactRouter({ + routes, + createHistory, + parseQueryString, + stringifyQuery, + routerStateSelector +}) { + return createStore => (reducer, initialState) => { + // `react-router`'s internal `history` + // is a bit different from the original `history` library. + const createAppHistory = useRouterHistory(createHistory); + + // Create `history` + const history = createAppHistory({ + parseQueryString, + stringifyQuery, + }); + + let store; + + // Create `transitionManager` which is gonna listen + // for `react-router` navigation events. + const transitionManager = createTransitionManager( + history, + createRoutes(getRoutes(routes, store)) + ); + + store = + applyMiddleware( + historyMiddleware(history) + )(createStore)(reducer, initialState); + + store.transitionManager = transitionManager; + store.history = history; + + // `react-router` state is held in `state.router` property by default + store[ROUTER_STATE_SELECTOR] = routerStateSelector; + + return store; + }; +} + +// Get `react-router` routes +function getRoutes(routes, store) { + if (!routes) { + throw new Error('"routes" parameter not passed to redux-router middleware'); + } + if (typeof routes === 'function') { + return routes({ + dispatch: action => store.dispatch(action), + getState: () => store.getState() + }); + } + return routes; +} \ No newline at end of file diff --git a/source/redux/redux-router/routerStateEquals.js b/source/redux/redux-router/routerStateEquals.js new file mode 100644 index 0000000..e5407ed --- /dev/null +++ b/source/redux/redux-router/routerStateEquals.js @@ -0,0 +1,18 @@ +import deepEqual from 'deep-equal'; +// import { DOES_NEED_REFRESH } from './constants'; + +/** + * Check if two router states are equal. Ignores `location.key`. + * @returns {Boolean} + */ +export default function routerStateEquals(a, b) { + if (!a && !b) return true; + if ((a && !b) || (!a && b)) return false; + // if (a[DOES_NEED_REFRESH] || b[DOES_NEED_REFRESH]) return false; + + return ( + a.location.pathname === b.location.pathname && + a.location.search === b.location.search && + deepEqual(a.location.state, b.location.state) + ); +} diff --git a/source/redux/redux-router/routerStateReducer.js b/source/redux/redux-router/routerStateReducer.js new file mode 100644 index 0000000..f5e8b56 --- /dev/null +++ b/source/redux/redux-router/routerStateReducer.js @@ -0,0 +1,27 @@ +import { + ROUTER_DID_CHANGE, + // REPLACE_ROUTES, + // DOES_NEED_REFRESH +} from './constants'; + +/** + * Reducer of ROUTER_DID_CHANGE actions. Returns a state object + * with { pathname, query, params, navigationType } + * @param {Object} state - Previous state + * @param {Object} action - Action + * @return {Object} New state + */ +export default function routerStateReducer(state = null, action) { + switch (action.type) { + case ROUTER_DID_CHANGE: + return action.payload; + // case REPLACE_ROUTES: + // if (!state) return state; + // return { + // ...state, + // [DOES_NEED_REFRESH]: true + // }; + default: + return state; + } +} diff --git a/source/redux/redux-router/server.js b/source/redux/redux-router/server.js new file mode 100644 index 0000000..9ef41e0 --- /dev/null +++ b/source/redux/redux-router/server.js @@ -0,0 +1,64 @@ +import { compose, applyMiddleware } from 'redux'; +import _reduxReactRouter from './reduxReactRouter'; +import useDefaults from './useDefaults'; +// import routeReplacement from './routeReplacement'; +import matchMiddleware from './matchMiddleware'; +import { MATCH } from './constants'; + +function addMatchingMiddleware(next) { + return options => createStore => (reducer, initialState) => { + const store = compose( + applyMiddleware( + matchMiddleware((url, callback) => { + const location = store.history.createLocation(url); + store.transitionManager.match(location, callback); + }) + ), + next(options) + )(createStore)(reducer, initialState); + return store; + }; +} + +export function match(url, callback) { + return { + type: MATCH, + payload: { + url, + callback + } + }; +} + +// Returns a function taking `options` +// and returning a Redux store enhancer +// (which is a composite of 3 store enhancers) +// +// This piece of code is very complex and very advanced. +// +// Redux store enhancers are a very advanced and complex and non-trivial stuff +// https://github.com/reactjs/redux/blob/master/src/applyMiddleware.js +// http://redux.js.org/docs/advanced/Middleware.html +// +export function reduxReactRouter(options) { + validateOptions(options); + options = useDefaults(options); + return addMatchingMiddleware(_reduxReactRouter)(options); +} + +function validateOptions(options) { + if (!options || !(options.routes || options.getRoutes)) { + throw new Error( + 'When rendering on the server, routes must be passed to the ' + + 'reduxReactRouter() store enhancer; routes as a prop or as children of ' + + ' is not supported. To deal with circular dependencies ' + + 'between routes and the store, use the option getRoutes(store).' + ); + } + if (!options || !(options.createHistory)) { + throw new Error( + 'When rendering on the server, createHistory must be passed to the ' + + 'reduxReactRouter() store enhancer' + ); + } +} \ No newline at end of file diff --git a/source/redux/redux-router/useDefaults.js b/source/redux/redux-router/useDefaults.js new file mode 100644 index 0000000..cfe004e --- /dev/null +++ b/source/redux/redux-router/useDefaults.js @@ -0,0 +1,21 @@ +const defaults = { + onError: error => { throw error; }, + routerStateSelector: state => state.router +}; + +export default function useDefaults(options) { + return { + ...defaults, + ...options, + createHistory: getCreateHistory(options) + }; +} + +function getCreateHistory({ createHistory, history }) { + if (typeof createHistory === 'function') { + return createHistory; + } + if (history) { + return () => history; + } +} \ No newline at end of file diff --git a/source/redux/server/create http client.js b/source/redux/server/create http client.js new file mode 100644 index 0000000..843da36 --- /dev/null +++ b/source/redux/server/create http client.js @@ -0,0 +1,17 @@ +import Http_client from '../../http client' + +// Isomorphic http client (with cookie support) +export default function create_http_client(common, authentication_token, application, request) +{ + return new Http_client + ({ + host : application ? application.host : undefined, + port : application ? application.port : undefined, + secure : application ? application.secure : false, + clone_request : request, + format_url : common.http && common.http.url, + parse_dates : common.parse_dates, + authentication_token, + authentication_token_header: common.authentication ? common.authentication.header : undefined + }) +} \ No newline at end of file diff --git a/source/redux/server/create store.js b/source/redux/server/create store.js new file mode 100644 index 0000000..3a144e8 --- /dev/null +++ b/source/redux/server/create store.js @@ -0,0 +1,31 @@ +import set_up_http_client from '../http client' +import _create_store from './store' + +export default function create_store(common, data, http_client) +{ + // create Redux store + const store = _create_store(common.reducer, + { + server: true, + routes: common.routes, + data, + middleware: common.redux_middleware, + on_store_created: common.on_store_created, + asynchronous_action_event_naming: common.asynchronous_action_event_naming, + on_preload_error : common.preload && common.preload.catch, + http_client, + preload_helpers : common.preload && common.preload.helpers, + on_navigate : common.on_navigate, + history_options : common.history + }) + + // Customization of `http` utility + // which can be used inside Redux action creators + set_up_http_client(http_client, + { + store, + on_before_send : common.http && common.http.request + }) + + return store +} \ No newline at end of file diff --git a/source/redux/server/render.js b/source/redux/server/render.js index 008d9e0..0d5f8bf 100644 --- a/source/redux/server/render.js +++ b/source/redux/server/render.js @@ -1,19 +1,7 @@ -import React from 'react' -import { match } from 'redux-router/server' -// import { ReduxRouter } from 'redux-router' - -// This is a temporary workaround for fixing `redux-router` -// to work with `react-router@3`. -// The maintainers of `redux-router` don't want to merge my pull requests, -// so I'm making this workaround here. -// https://github.com/acdlite/redux-router/pull/282 -const createRouterObject = require('react-router/lib/RouterUtils').createRouterObject -require('react-router/lib/RouterUtils').createRouterObject = function(history, transitionManager, state = {}) -{ - return createRouterObject(history, transitionManager, state) -} - -const ReduxRouter = require('redux-router').ReduxRouter +import React from 'react' +// import { match } from '../redux-router/server' +// import { ReduxRouter } from '../redux-router' +import { Router, match } from 'react-router' import react_render_on_server from '../../render on server' @@ -34,7 +22,7 @@ function timed_react_render_on_server(named_arguments) // Returns a Promise resolving to { status, content, redirect }. // -export default function render_on_server({ disable_server_side_rendering, create_page_element, render_webpage, url, store }) +export default function render_on_server({ disable_server_side_rendering, create_page_element, render_webpage, url, store, routes }) { // Routing only takes a couple of milliseconds // const routing_timer = timer() @@ -44,7 +32,8 @@ export default function render_on_server({ disable_server_side_rendering, create const preload_timer = timer() // Perform routing for this `url` - return match_url(url, store).then(routing_result => + // return match_url(url, store).then(routing_result => + return match_url(url, store, routes).then(routing_result => { // routing_timer() @@ -55,11 +44,11 @@ export default function render_on_server({ disable_server_side_rendering, create } // Http response status code - const http_status_code = get_http_response_status_code_for_the_route(routing_result.matched_routes) + const http_status_code = get_http_response_status_code_for_the_route(routing_result.router_state.routes) // Concatenated `react-router` route string. // E.g. "/user/:user_id/post/:post_id" - const route = routing_result.matched_routes + const route = routing_result.router_state.routes .filter(route => route.path) .map(route => route.path.replace(/^\//, '').replace(/\/$/, '')) .join('/') || '/' @@ -73,7 +62,8 @@ export default function render_on_server({ disable_server_side_rendering, create // // After the page has finished preloading, render it // - return wait_for_page_to_preload(store).then(() => + // return wait_for_page_to_preload(store).then(() => + return store.dispatch({ type: '@@react-isomorphic-render/preload', location: url }).then(() => { time.preload = preload_timer() @@ -89,7 +79,8 @@ export default function render_on_server({ disable_server_side_rendering, create // Renders the current page React component to a React element // (`` is gonna get the matched route from the `store`) - const page_element = create_page_element(, { store }) + const page_element = create_page_element(, { store }) + // const page_element = create_page_element(, { store }) // Render the current page's React element to HTML markup const rendered = timed_react_render_on_server({ render_webpage, page_element }) @@ -117,23 +108,23 @@ export default function render_on_server({ disable_server_side_rendering, create }) } -// Waits for all `@preload()` calls to finish. -function wait_for_page_to_preload(store) -{ - // This promise was previously set by "preloading middleware" - // if there were any @preload() calls on the current route components - const promise = store.getState().router +// // Waits for all `@preload()` calls to finish. +// function wait_for_page_to_preload(store) +// { +// // This promise was previously set by "preloading middleware" +// // if there were any @preload() calls on the current route components +// const promise = store.getState().router - // Validate the currently preloading promise - if (promise && typeof promise.then === 'function') - { - // If it's really a Promise then return it - return promise - } +// // Validate the currently preloading promise +// if (promise && typeof promise.then === 'function') +// { +// // If it's really a Promise then return it +// return promise +// } - // Otherwise, if nothing is being preloaded, just return a dummy Promise - return Promise.resolve() -} +// // Otherwise, if nothing is being preloaded, just return a dummy Promise +// return Promise.resolve() +// } // One can set a `status` prop for a react-router `Route` // to be returned as an Http response status code (404, etc) @@ -151,17 +142,21 @@ function get_http_response_status_code_for_the_route(matched_routes) // // matched_routes - the matched hierarchy of React-router ``s // -function match_url(url, store) +// function match_url(url, store) +function match_url(url, store, routes) { + routes = typeof routes === 'function' ? routes(store) : routes + // (not using `promisify()` helper here // to avoid introducing dependency on `bluebird` Promise library) // return new Promise((resolve, reject) => { - // perform routing for this `url` - store.dispatch(match(url, (error, redirect_location, router_state) => + // Perform routing for this `url` + // store.dispatch(match(url, (error, redirect_location, router_state) => + match({ routes, location: url }, (error, redirect_location, router_state) => { - // if a decision to perform a redirect was made + // If a decision to perform a redirect was made // during the routing process, // then redirect to another url if (redirect_location) @@ -172,19 +167,20 @@ function match_url(url, store) }) } - // routing process failed + // Routing process failed if (error) { return reject(error) } - // don't know what this if condition is for + // In case some weird stuff happened if (!router_state) { return reject(new Error('No router state')) } - return resolve({ matched_routes: router_state.routes }) - })) + // return resolve({ matched_routes: router_state.routes }) + return resolve({ router_state }) + }) }) } \ No newline at end of file diff --git a/source/redux/server/store.js b/source/redux/server/store.js index 05c1a6c..8f0dbde 100644 --- a/source/redux/server/store.js +++ b/source/redux/server/store.js @@ -1,8 +1,10 @@ -import { reduxReactRouter } from 'redux-router/server' +// import { reduxReactRouter } from '../redux-router/server' import createHistory from 'history/lib/createMemoryHistory' import create_store from '../store' +// reduxReactRouter, + export default function create_store_on_server(reducer, settings) { - return create_store(reduxReactRouter, createHistory, reducer, settings) + return create_store(createHistory, reducer, settings) } \ No newline at end of file diff --git a/source/redux/store.js b/source/redux/store.js index ab7b1cc..eff6810 100644 --- a/source/redux/store.js +++ b/source/redux/store.js @@ -1,15 +1,19 @@ import { createStore, combineReducers, applyMiddleware, compose } from 'redux' -import { routerStateReducer } from 'redux-router' +// import { routerStateReducer } from './redux-router' import { createRoutes } from 'react-router/lib/RouteUtils' -import { useBasename } from 'history' +// import { useBasename } from 'history' import asynchronous_middleware from './middleware/asynchronous middleware' import preloading_middleware from './middleware/preloading middleware' -import on_route_update_middleware from './middleware/on route update middleware' +// import on_route_update_middleware from './middleware/on route update middleware' +import history_middleware from './middleware/history middleware' + +import { useRouterHistory } from 'react-router' // import use_scroll from 'scroll-behavior' -export default function create_store(reduxReactRouter, createHistory, reducer, { devtools, server, data, routes, http_client, promise_event_naming, on_preload_error, middleware, on_store_created, preload_helpers, on_navigate, history_options }) +// reduxReactRouter, +export default function create_store(createHistory, reducer, { devtools, server, data, routes, http_client, asynchronous_action_event_naming, on_preload_error, middleware, on_store_created, preload_helpers, history_options }) { // Simply using `useScroll` from `scroll-behavior@0.7.0` // introduces scroll jumps to top when navigating the app @@ -20,12 +24,6 @@ export default function create_store(reduxReactRouter, createHistory, reducer, { // // Therefore using a middleware to wait for page loading to finish. - // Generates the three promise event names automatically based on a base event name - if (!promise_event_naming) - { - promise_event_naming = event_name => [`${event_name}: pending`, `${event_name}: done`, `${event_name}: failed`] - } - // Redux store enhancers const store_enhancers = [] @@ -39,6 +37,10 @@ export default function create_store(reduxReactRouter, createHistory, reducer, { } } + const history = useRouterHistory(createHistory)(history_options) + + // Redux middlewares are applied in reverse order + // (which is counter-intuitive) const middlewares = [ // Enables support for Ajax Http requests. @@ -54,7 +56,7 @@ export default function create_store(reduxReactRouter, createHistory, reducer, { // therefore there's an additional `dispatch_event` argument // which is a function to hack around that limitation. // - asynchronous_middleware(http_client, event => store.dispatch(event), { promise_event_naming }), + asynchronous_middleware(http_client, event => store.dispatch(event), { asynchronous_action_event_naming }), // Enables support for @preload() annotation // (which preloads data required for displaying certain pages). @@ -70,61 +72,51 @@ export default function create_store(reduxReactRouter, createHistory, reducer, { // won't send actions to that `reduxReactRouter` middleware, // therefore using the third argument to hack around this thing. // - preloading_middleware(server, on_preload_error, event => store.dispatch(event), preload_helpers) + preloading_middleware(server, on_preload_error, event => store.dispatch(event), preload_helpers, routes, history), + + history_middleware(history) ] - if (on_navigate) - { - middlewares.push - ( - // Implements `react-router` `onUpdate` handler - // - // Listens for `{ type: ROUTER_DID_CHANGE }` - // - on_route_update_middleware(on_navigate) - ) - } + // if (on_navigate && !server) + // { + // // Redux middlewares are applied in reverse order + // // (which is counter-intuitive) + // middlewares.push + // ( + // // Implements `react-router` `onUpdate` handler + // // + // // Listens for `{ type: ROUTER_DID_CHANGE }` + // // + // on_route_update_middleware(on_navigate, history) + // ) + // } + // Redux middlewares are applied in reverse order + // (which is counter-intuitive) store_enhancers.push ( - // `redux-router` middleware - // (redux-router keeps react-router state in Redux) - reduxReactRouter - ({ - getRoutes: ({ dispatch, getState }) => - { - function get_state() - { - try - { - return getState() - } - // "TypeError: Cannot read property 'getState' of undefined". - // This error is thrown when calling `getState()` - // directly inside `getRoutes()`, before the `store` is created. - catch (error) - { - // Pretend that Redux state is the - // result of `initialize` function call. - return data - } - } - - return typeof routes === 'function' ? routes({ dispatch, getState: get_state }) : routes; - }, - createHistory: (...parameters) => - { - return useBasename(createHistory)({ ...parameters, ...history_options }) - } - }), - - // Ajax and @preload middleware (+ optional others) + // // `redux-router` middleware + // // (redux-router keeps react-router state in Redux) + // reduxReactRouter + // ({ + // routes, + // createHistory(...parameters) + // { + // return useBasename(createHistory)({ ...parameters, ...history_options }) + // } + // }), + + // Ajax and @preload middleware (+ optional others). + // Redux middlewares are applied in reverse order + // (which is counter-intuitive) applyMiddleware(...middlewares) ) // Add Redux DevTools (if they're enabled) if (process.env.NODE_ENV !== 'production' && !server && devtools) { + // Redux middlewares are applied in reverse order + // (which is counter-intuitive) store_enhancers.push ( // Provides support for DevTools @@ -136,17 +128,19 @@ export default function create_store(reduxReactRouter, createHistory, reducer, { // adds redux-router reducers to the list of all reducers. // overall Redux reducer = web application reducers + redux-router reducer - const overall_reducer = () => + const overall_reducer = (reducer) => { const reducers = typeof reducer === 'function' ? reducer() : reducer - reducers.router = routerStateReducer + // reducers.router = routerStateReducer return combineReducers(reducers) } // create Redux store // with the overall Redux reducer // and the initial Redux store data (aka "the state") - const store = compose(...store_enhancers)(createStore)(overall_reducer(), data) + const store = compose(...store_enhancers)(createStore)(overall_reducer(reducer), data) + + store.history = history // Because History API won't work on the server side, // instrument it with redirection handlers (isomorphic redirection) @@ -191,21 +185,13 @@ export default function create_store(reduxReactRouter, createHistory, reducer, { // { // module.hot.accept(reducers_path, () => // { - // store.replaceReducer(overall_reducer()) + // store.replaceReducer(overall_reducer(reducer)) // }) // } - // `reload` helper function gives the web application means to hot reload its Redux reducers - if (on_store_created) - { - const reload_reducer = () => store.replaceReducer(overall_reducer()) - - on_store_created - ({ - reload_reducer, - reloadReducer: reload_reducer - }) - } + // `hot_reload` helper function gives the web application means to hot reload its Redux reducers + store.hot_reload = reducer => store.replaceReducer(overall_reducer(reducer)) + store.hotReload = store.hot_reload // return the created Redux store return store diff --git a/test/exports.js b/test/exports.js index 2fe9f51..b274504 100644 --- a/test/exports.js +++ b/test/exports.js @@ -18,7 +18,16 @@ import Preload_method_name, PRELOAD_METHOD_NAME, Preload_options_name, - PRELOAD_OPTIONS_NAME + PRELOAD_OPTIONS_NAME, + action, + create_handler, + createHandler, + state_connector, + stateConnector, + underscoredToCamelCase, + event_name, + eventName, + Link } from '../index.es6' @@ -47,6 +56,18 @@ describe(`exports`, function() PRELOAD_METHOD_NAME.should.be.a('string') Preload_options_name.should.be.a('string') PRELOAD_OPTIONS_NAME.should.be.a('string') + + action.should.be.a('function') + create_handler.should.be.a('function') + createHandler.should.be.a('function') + state_connector.should.be.a('function') + stateConnector.should.be.a('function') + + underscoredToCamelCase.should.be.a('function') + event_name.should.be.a('function') + eventName.should.be.a('function') + + Link.should.be.a('function') }) it(`should export ES5`, () => @@ -77,27 +98,17 @@ describe(`exports`, function() _.Preload_options_name.should.be.a('string') _.PRELOAD_OPTIONS_NAME.should.be.a('string') - // Backwards compatibility for `/redux` export - // (will be removed in version 9.0.0) - const redux = require('../redux') - - redux.render.should.be.a('function') - redux.preload.should.be.a('function') - - redux.goto.should.be.a('function') - redux.redirect.should.be.a('function') + _.action.should.be.a('function') + _.create_handler.should.be.a('function') + _.createHandler.should.be.a('function') + _.state_connector.should.be.a('function') + _.stateConnector.should.be.a('function') - redux.Preload_started.should.be.a('string') - redux.PRELOAD_STARTED.should.be.a('string') - redux.Preload_finished.should.be.a('string') - redux.PRELOAD_FINISHED.should.be.a('string') - redux.Preload_failed.should.be.a('string') - redux.PRELOAD_FAILED.should.be.a('string') + _.underscoredToCamelCase.should.be.a('function') + _.event_name.should.be.a('function') + _.eventName.should.be.a('function') - redux.Preload_method_name.should.be.a('string') - redux.PRELOAD_METHOD_NAME.should.be.a('string') - redux.Preload_options_name.should.be.a('string') - redux.PRELOAD_OPTIONS_NAME.should.be.a('string') + _.Link.should.be.a('function') }) it(`should export rendering service`, () =>