Skip to content

Commit

Permalink
Refactoring for 9.1. Fixed redirection.
Browse files Browse the repository at this point in the history
  • Loading branch information
catamphetamine committed Jan 13, 2017
1 parent e3a45bb commit 62e3824
Show file tree
Hide file tree
Showing 29 changed files with 771 additions and 618 deletions.
14 changes: 10 additions & 4 deletions HISTORY.md
@@ -1,13 +1,19 @@
9.0.0 / 10.01.2017
9.1.0 / 13.01.2017
==================

A couple of TODOs for `9.0.0`:
* (small breaking change) server-side `localize()` function parameter now takes not just `store` argument but instead a wrapped `{ store }` argument.
* (small breaking change) server-side `assets`, `head`, `bodyStart` and `bodyEnd` now take not the old `url` argument but instead the new `path` argument (aka `pathname`), because query parameters should be irrelevant for code-splitting and customization.
* (small breaking change) `preload.catch` now doesn't receive `redirect` parameter: use `dispatch(goto(url))` instead.
* Added `path` to `preload.error` handler parameters.
* Added `goto` parameter function to `preload.catch`.

A couple of TODOs for `9.x`:

* Fix `@preload()` with programmatic `redirect` and `goto` (almost done)
* Fix `onEnter` being called twice (both on server and client, because `react-router`'s `match()` is called there twice) - this is not considered a blocker for `9.0.0` release since `@preload()` supercedes `onEnter` and therefore `onEnter` may not be used at all. I guess it can be fixed using `<RouterContext>`.
* Maybe implement the minor `previous_route_components` optimization from `8.0.0` for preloading pages

Changes:
9.0.0 / 10.01.2017
==================

* Added "asynchronous action handlers" (see README)
<!--* Since `redux-router` maintainers are incompetent and lazy, they don't want to merge my Pull Requests, I'm forking `redux-router` repo as part of this library (`./source/redux/redux-router`) and making the neccessary changes to the code.-->
Expand Down
7 changes: 7 additions & 0 deletions PHILOSOPHY.md
@@ -0,0 +1,7 @@
Early attempts at marrying React-Router and Redux built itself upon an idea that React-Router state should be managed by Redux: the current location was part of Redux state and when that part of Redux state changed then the routing would be performed, therefore a user could perform navigation just by `dispatch()`ing an action, and also had an easy access to the current location in Redux state.

While this is an elegant and smart solution, still I don't feel like React-Router state really belongs to Redux application state. It's like two parallel worlds: the navigation world and the application model world. They don't depend on each other in any way and they're conceptually different and not interconnected. So, I think that React-Router should manage its state by itself.

Then, one may ask, why is this library using `dispatch()` for redirection and navigation? And for page preloading?

The answer is: it could easily be some random global variable instead, like `window.__history__` and `redirect()` would just call `window.__history__.replace()`, but, since javascript is single-threaded, on the server side it simply wouldn't work because that global `window.__history__` variable would be shared among all clients which would result in a really weird navigation for all of them. Ok, so the solution is actually simple: just declare some `var history_storage = { history }` and then just pass it around in all functions. This would work, but it would not only introduce a lot of spaghetti code but also would really be a re-implementation of already existing Redux store. And why invent another wheel then. Just use the one you already have if it doesn't interfere (and it doesn't). So that the reason why this library has `dispatch()` for navigation but still doesn't store router state in Redux state.
58 changes: 35 additions & 23 deletions README.md
Expand Up @@ -44,19 +44,32 @@ $ npm install react-isomorphic-render --save

(see [webapp](https://github.com/halt-hammerzeit/webapp) and [webpack-react-redux-isomorphic-render-example](https://github.com/halt-hammerzeit/webpack-react-redux-isomorphic-render-example) as references)

Start by creating your `react-isomorphic-render.js` set up file (it configures both client side and server side)
Start by creating a settings file (it configures both client side and server side)

#### react-isomorphic-render.js

```javascript
export default {
// Redux reducers
reducer: require('./src/client/redux/reducers'),
// React-Router routes
routes: require('./src/client/routes'),

// React-router routes
routes: require('./src/client/routes')
// Redux reducers
// (they will be combined via `combineReducers()`)
reducer: require('./src/client/redux/reducers')
}
```

Then create your client-side main application file (`application.js`)
#### ./src/client/redux/reducers/index.js

```js
export { default as reducer1 } from './reducer1'
export { default as reducer2 } from './reducer2'
...
```

Then call `render()` in the main client-side javascript file

#### ./src/client/application.js

```javascript
// Include CSS styles in the bundle
Expand Down Expand Up @@ -237,19 +250,15 @@ try {
//
const { status, content, redirect } = await render(settings, {
// Takes the same parameters as webpage server
application: { host, port },
assets,
...

// Original HTTP request, which is used for
// getting URL, cloning cookies, and inside `initialize`.
request,

// Cookies object with `.get(name)` function
// Cookies object having `.get(name)` function
// (only needed if using `authentication` cookie feature)
cookies,

// The rest optional parameters are the same
// as for webpage server and are all optional
cookies
})

if (redirect) {
Expand Down Expand Up @@ -954,15 +963,14 @@ If you're using Webpack then make sure you either build your server-side code wi

```javascript
{
// Redux reducers (an object of reducing functions).
// (either a reducers object or a function returning a reducers object)
reducer: require('./src/client/redux/reducers')

// React-router routes
// (either a `<Route/>` element or a
// `function({ dispatch, getState })`
// returning a `<Route/>` element)
routes: require('./src/client/routes')

// Redux reducers (an object)
reducer: require('./src/client/redux/reducers')

// A React component.
//
Expand Down Expand Up @@ -1031,7 +1039,7 @@ If you're using Webpack then make sure you either build your server-side code wi
// then a redirect to "/unauthorized" page can be made here.
// If this error handler is defined then it must handle
// all errors it gets (or just re`throw` them).
catch: (error, { url, redirect, dispatch, getState }) => redirect(`/error?url=${encode(url)}&error=${error.status}`)
catch: (error, { path, url, dispatch, getState }) => dispatch(goto(`/error?url=${encode(url)}&error=${error.status}`))
}

// (optional)
Expand Down Expand Up @@ -1122,14 +1130,14 @@ If you're using Webpack then make sure you either build your server-side code wi
//
// Also a website "favicon" URL, if any.
//
// Can be an `object` or a `function(url, { store })`.
// Can be an `object` or a function returning an object.
//
// `javascript` and `style` can be strings or objects.
// If they are objects then one should also provide an `entry` parameter.
// The objects may also contain `common` entry
// which will also be included on the page.
//
assets: (url, { store }) =>
assets: (path, { store }) =>
{
return {
javascript: '/assets/main.js',
Expand Down Expand Up @@ -1161,18 +1169,18 @@ If you're using Webpack then make sure you either build your server-side code wi
// (optional)
// Markup inserted into server rendered webpage's <head/>.
// Can be either a function returning a value or just a value.
head: (url, { store }) => String, or React.Element, or an array of React.Elements
head: (path, { store }) => String, or React.Element, or an array of React.Elements

// (optional)
// Markup inserted to the start of the server rendered webpage's <body/>.
// Can be either a function returning a value or just a value.
bodyStart: (url, { store }) => String, or React.Element, or an array of React.Elements
bodyStart: (path, { store }) => String, or React.Element, or an array of React.Elements
// (aka `body_start`)

// (optional)
// Markup inserted to the end of the server rendered webpage's <body/>.
// Can be either a function returning a value or just a value.
bodyEnd: (url, { store }) => String, or React.Element, or an array of React.Elements
bodyEnd: (path, { store }) => String, or React.Element, or an array of React.Elements
// (aka `body_end`)
}

Expand Down Expand Up @@ -1292,6 +1300,10 @@ Client-side `render` function returns a `Promise` resolving to an object
}
```

## For purists

See [PHILOSOPHY](https://github.com/halt-hammerzeit/react-isomorphic-render/blob/master/PHILOSOPHY.md)

## Contributing

After cloning this repo, ensure dependencies are installed by running:
Expand Down
4 changes: 1 addition & 3 deletions package.json
@@ -1,17 +1,15 @@
{
"name": "react-isomorphic-render",
"version": "9.0.10",
"version": "9.1.0",
"description": "Isomorphic rendering with React, Redux, React-router and Redux-router. Includes support for Webpack",
"main": "index.common.js",
"module": "index.es6.js",
"dependencies": {
"babel-runtime": "^6.6.1",
"deep-equal": "^1.0.1",
"hoist-non-react-statics": "^1.0.5",
"koa": "^2.0.0",
"nunjucks": "^3.0.0",
"print-error": "^0.1.3",
"query-string": "^4.2.3",
"react-helmet": "^3.2.3",
"react-router-scroll": "^0.4.1",
"superagent": "^2.1.0",
Expand Down
3 changes: 0 additions & 3 deletions server.js
Expand Up @@ -6,7 +6,4 @@ 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
20 changes: 16 additions & 4 deletions source/client.js
@@ -1,12 +1,16 @@
import React from 'react'
import createHistory from 'history/lib/createBrowserHistory'
import { useRouterHistory } from 'react-router'

import _create_history from './history'

// Performs client-side rendering
// along with varios stuff like loading localized messages.
//
// This function is intended to be wrapped by another function
// which (in turn) is gonna be called from the project's code on the client-side.
//
export default function localize_and_render({ render_parameters = {}, render_on_client, wrapper, translation })
export default function client_side_render({ history, render, render_parameters = {}, wrapper, translation })
{
// Make sure authentication token global variable is erased
// (in case it hasn't been read and erased before)
Expand All @@ -19,6 +23,7 @@ export default function localize_and_render({ render_parameters = {}, render_on_
delete window._locale
}

// Localized messages
let messages = window._locale_messages
if (messages)
{
Expand All @@ -31,9 +36,10 @@ export default function localize_and_render({ render_parameters = {}, render_on_
{
// returns a Promise for React component.
//
return render_on_client
return render
({
...render_parameters,
to: document.getElementById('react'),
create_page_element : async (element, props = {}) =>
{
// if no i18n is required, then simply create Page element
Expand All @@ -57,8 +63,7 @@ export default function localize_and_render({ render_parameters = {}, render_on_

// create React page element
return React.createElement(wrapper, props, element)
},
to: document.getElementById('react')
}
})
}

Expand Down Expand Up @@ -86,4 +91,11 @@ export function authentication_token()
}

return token
}

// Create `react-router` `history`
export function create_history(location, settings)
{
// Adds 'useBasename' and 'useQueries'
return _create_history(useRouterHistory(createHistory), location, settings.history)
}
79 changes: 79 additions & 0 deletions source/history.js
@@ -0,0 +1,79 @@
import { location_url } from './location'

// Creates `history`
export default function create_history(createHistory, location, history_options, server)
{
// Create `history`.
// https://github.com/ReactTraining/react-router/blob/master/docs/guides/Histories.md#customize-your-history-further
const history = createHistory({ ...history_options, entries: [ location ] })

// Because History API won't work on the server side for navigation,
// instrument it with custom redirection handlers.
if (server)
{
// Instrument `history`
// (which was earlier passed to `preloading_middleware`)
history.replace = server_side_redirect(history_options && history_options.basename)
history.push = history.replace
}

// Return `history`
return history
}

// A hacky way but it should work
// for calling `redirect` from anywhere
// inside `@preload()` function argument.
function server_side_redirect(basename)
{
return (location) =>
{
// Sanity check
if (!location)
{
throw new Error(`location parameter is required for redirect() or goto()`)
}

// Convert an object to a textual URL
let url = location_url(location)

// If it's a relative URL, then prepend `basename` to it.
// (imulates `history` `basename` functionality)
if (url[0] === '/' && basename)
{
url = `${basename}${url}`
}

// Construct a special "Error" used for aborting and redirecting
server_redirect(url)
}
}

export function get_location(history)
{
// v4
if (history.location)
{
return history.location
}

// v3
if (history.getCurrentLocation)
{
return history.getCurrentLocation()
}

// v2
let location
const unlisten = history.listen(x => location = x)
unlisten()
return location
}

export function server_redirect(location)
{
const url = location_url(location)
const error = new Error(`Redirecting to ${url} (this is not an error)`)
error._redirect = url
throw error
}

0 comments on commit 62e3824

Please sign in to comment.