diff --git a/.babelrc b/.babelrc index 2d4d503..63d065b 100644 --- a/.babelrc +++ b/.babelrc @@ -1,3 +1,3 @@ { - "presets": ["es2015", "stage-2"] + "presets": ["es2015", "stage-1"] } diff --git a/.gitignore b/.gitignore index 7c4bc80..b2cc0b3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ lib node_modules coverage +*.log diff --git a/README.md b/README.md index 6e2e09c..5da0d53 100644 --- a/README.md +++ b/README.md @@ -2,37 +2,31 @@ [![npm version](https://img.shields.io/npm/v/react-router-redux.svg?style=flat-square)](https://www.npmjs.com/package/react-router-redux) [![npm downloads](https://img.shields.io/npm/dm/react-router-redux.svg?style=flat-square)](https://www.npmjs.com/package/react-router-redux) [![build status](https://img.shields.io/travis/reactjs/react-router-redux/master.svg?style=flat-square)](https://travis-ci.org/reactjs/react-router-redux) -**Let react-router do all the work** :sparkles: +> **Keep your router in sync with application state** :sparkles: _Formerly known as redux-simple-router_ -[Redux](https://github.com/rackt/redux) is awesome. [React Router](https://github.com/reactjs/react-router) is cool. The problem is that react-router manages an important piece of your application state: the URL. If you are using redux, you want your app state to fully represent your UI; if you snapshotted the app state, you should be able to load it up later and see the same thing. +You're a smart person. You use [Redux](https://github.com/rackt/redux) to manage your application state. You use [React Router](https://github.com/reactjs/react-router) to do routing. All is good. -react-router does a great job of mapping the current URL to a component tree, and continually does so with any URL changes. This is very useful, but we really want to store this state in redux as well. +But the two libraries don't coordinate. You want to do time travel with your application state, but React Router doesn't navigate between pages when you replay actions. It controls an important part of application state: the URL. -The entire state that we are interested in boils down to one thing: the URL. This is an extremely simple library that just puts the URL in redux state and keeps it in sync with any react-router changes. Additionally, you can change the URL via redux and react-router will change accordingly. +This library helps you keep that bit of state in sync with your Redux store. We keep a copy of the current location hidden in state. When you rewind your application state with a tool like [Redux DevTools](https://github.com/gaearon/redux-devtools), that state change is propagated to React Router so it can adjust the component tree accordingly. You can jump around in state, rewinding, replaying, and resetting as much as you'd like, and this library will ensure the two stay in sync at all times. + +## Installation ``` -npm install react-router-redux +npm install --save react-router-redux ``` If you want to install the next major version, use `react-router-redux@next`. Run `npm dist-tag ls react-router-redux` to see what `next` is aliased to. -View the [CHANGELOG](https://github.com/reactjs/react-router-redux/blob/master/CHANGELOG.md) for recent changes. - -Read the [API docs](#api) farther down this page. - -**Note:** We are [currently working on some major changes/improvements](https://github.com/reactjs/react-router-redux/issues/259) to the library. [React Router's API in 2.0](https://github.com/reactjs/react-router/blob/master/upgrade-guides/v2.0.0.md) is significantly improved and obseletes the need for things like action creators and reading location state from the Redux. This library is still critical to enable things like time traveling and persisting state, so we're not going anywhere. But in many cases, you may not need this library and can simply use the provided React Router APIs. Go check them out and drop some technical debt. :smile: - -### Usage +## How It Works -The idea of this library is to use react-router's functionality exactly like its documentation tells you to. You can access all of its APIs in routing components. Additionally, you can use redux like you normally would, with a single app state. +This library allows you to use React Router's APIs as they are documented. And, you can use redux like you normally would, with a single app state. The library simply enhances a history instance to allow it to synchronize any changes it receives into application state. -[redux](https://github.com/rackt/redux) (`store.routing`)  ↔  [**react-router-redux**](https://github.com/reactjs/react-router-redux)  ↔  [history](https://github.com/reactjs/history) (`history.location`)  ↔  [react-router](https://github.com/reactjs/react-router) +[history](https://github.com/reactjs/history) + `store` ([redux](https://github.com/rackt/redux)) → [**react-router-redux**](https://github.com/reactjs/react-router-redux) → enhanced [history](https://github.com/reactjs/history) → [react-router](https://github.com/reactjs/react-router) -We only store current URL and state, whereas redux-router stores the entire location object from react-router. You can read it, and also change it with an action. - -### Tutorial +## Tutorial Let's take a look at a simple example. @@ -44,25 +38,25 @@ import ReactDOM from 'react-dom' import { createStore, combineReducers, applyMiddleware } from 'redux' import { Provider } from 'react-redux' import { Router, Route, browserHistory } from 'react-router' -import { syncHistory, routeReducer } from 'react-router-redux' -import reducers from '/reducers' - -const reducer = combineReducers(Object.assign({}, reducers, { - routing: routeReducer -})) +import { syncHistoryWithStore, routerReducer } from 'react-router-redux' -// Sync dispatched route actions to the history -const reduxRouterMiddleware = syncHistory(browserHistory) -const createStoreWithMiddleware = applyMiddleware(reduxRouterMiddleware)(createStore) +import reducers from '/reducers' -const store = createStoreWithMiddleware(reducer) +// Add the reducer to your store on the `routing` key +const store = createStore( + combineReducers({ + ...reducers, + routing: routerReducer + }) +) -// Required for replaying actions from devtools to work -reduxRouterMiddleware.listenForReplays(store) +// Create an enhanced history that syncs navigation events with the store +const history = syncHistoryWithStore(browserHistory, store) ReactDOM.render( - + /* Tell the Router to use our enhanced history */ + @@ -73,31 +67,32 @@ ReactDOM.render( ) ``` -Now you can read from `state.routing.location.pathname` to get the URL. It's far more likely that you want to change the URL more often, however. You can use the `push` action creator that we provide: +Now any time you navigate, which can come from pressing browser buttons or navigating in your application code, the enhanced history will first pass the new location through the Redux store and then on to React Router to update the component tree. If you time travel, it will also pass the new state to React Router to update the component tree again. + +#### How do I watch for navigation events, such as for analytics? + +Simply listen to the enhanced history via `history.listen`. This takes in a function that will receive a `location` any time the store updates. This includes any time travel activity performed on the store. ```js -import { routeActions } from 'react-router-redux' +const history = syncHistoryWithStore(browserHistory, store) -function MyComponent({ dispatch }) { - return +
{children}
) } - -export default connect( - null, - routeActions -)(App) diff --git a/examples/basic/index.html b/examples/basic/index.html index 9bc05ca..5452876 100644 --- a/examples/basic/index.html +++ b/examples/basic/index.html @@ -6,6 +6,6 @@
- + diff --git a/examples/basic/package.json b/examples/basic/package.json index ba0db2f..9458dab 100644 --- a/examples/basic/package.json +++ b/examples/basic/package.json @@ -4,29 +4,30 @@ "repository": "reactjs/react-router-redux", "license": "MIT", "dependencies": { - "react": "^0.14.2", - "react-dom": "^0.14.2", - "react-redux": "^4.0.0", + "react": "^0.14.7", + "react-dom": "^0.14.7", + "react-redux": "^4.3.0", "react-router": "^2.0.0", - "redux": "^3.0.4", - "react-router-redux": "^2.1.0" + "redux": "^3.2.1", + "react-router-redux": "^3.0.0" }, "devDependencies": { - "babel-core": "^6.1.21", - "babel-eslint": "^5.0.0-beta6", - "babel-loader": "^6.2.0", - "babel-preset-es2015": "^6.1.18", - "babel-preset-react": "^6.1.18", + "babel-core": "^6.4.5", + "babel-eslint": "^5.0.0-beta9", + "babel-loader": "^6.2.2", + "babel-preset-es2015": "^6.3.13", + "babel-preset-react": "^6.3.13", "babel-preset-stage-1": "^6.3.13", "eslint": "^1.10.3", "eslint-config-rackt": "^1.1.1", - "eslint-plugin-react": "^3.15.0", - "redux-devtools": "^3.0.0", + "eslint-plugin-react": "^3.16.1", + "redux-devtools": "^3.1.0", "redux-devtools-dock-monitor": "^1.0.1", - "redux-devtools-log-monitor": "^1.0.1", - "webpack": "^1.12.6" + "redux-devtools-log-monitor": "^1.0.4", + "webpack": "^1.12.13", + "webpack-dev-server": "^1.14.1" }, "scripts": { - "start": "webpack --watch" + "start": "webpack-dev-server --history-api-fallback --no-info --open" } } diff --git a/examples/server/.babelrc b/examples/server/.babelrc new file mode 100644 index 0000000..a1e122a --- /dev/null +++ b/examples/server/.babelrc @@ -0,0 +1,8 @@ +{ + "presets": ["es2015", "react", "stage-1"], + "plugins": [ + ["babel-plugin-module-alias", [ + { "src": "../../src", "expose": "react-router-redux" } + ]] + ] +} diff --git a/examples/server/client.js b/examples/server/client.js new file mode 100644 index 0000000..a2940b2 --- /dev/null +++ b/examples/server/client.js @@ -0,0 +1,28 @@ +import 'babel-polyfill' + +import React from 'react' +import { render } from 'react-dom' + +import { Provider } from 'react-redux' +import { Router, browserHistory } from 'react-router' +import { syncHistoryWithStore } from 'react-router-redux' + +import { configureStore, DevTools } from './store' +import routes from './routes' + +const store = configureStore(browserHistory, window.__initialState__) +const history = syncHistoryWithStore(browserHistory, store) + +render( + + + , + document.getElementById('root') +) + +render( + + + , + document.getElementById('devtools') +) diff --git a/examples/server/index.html b/examples/server/index.html new file mode 100644 index 0000000..fa87359 --- /dev/null +++ b/examples/server/index.html @@ -0,0 +1,11 @@ + + + + react-router-redux server rendering example + + + +
+ + + diff --git a/examples/server/package.json b/examples/server/package.json new file mode 100644 index 0000000..82ec6d8 --- /dev/null +++ b/examples/server/package.json @@ -0,0 +1,38 @@ +{ + "name": "rrr-server-example", + "version": "0.0.0", + "repository": "reactjs/react-router-redux", + "license": "MIT", + "dependencies": { + "react": "^0.14.7", + "react-dom": "^0.14.7", + "react-redux": "^4.3.0", + "react-router": "^2.0.0", + "react-router-redux": "^3.0.0", + "redux": "^3.2.1", + "serialize-javascript": "^1.1.2" + }, + "devDependencies": { + "babel-cli": "^6.5.1", + "babel-core": "^6.4.5", + "babel-eslint": "^5.0.0-beta9", + "babel-loader": "^6.2.2", + "babel-plugin-module-alias": "^1.2.0", + "babel-preset-es2015": "^6.3.13", + "babel-preset-react": "^6.3.13", + "babel-preset-stage-1": "^6.3.13", + "babel-register": "^6.5.2", + "eslint": "^1.10.3", + "eslint-config-rackt": "^1.1.1", + "eslint-plugin-react": "^3.16.1", + "express": "^4.13.4", + "redux-devtools": "^3.1.1", + "redux-devtools-dock-monitor": "^1.0.1", + "redux-devtools-log-monitor": "^1.0.4", + "webpack": "^1.12.13", + "webpack-dev-middleware": "^1.5.1" + }, + "scripts": { + "start": "babel-node server.js" + } +} diff --git a/examples/server/routes.js b/examples/server/routes.js new file mode 100644 index 0000000..5164adf --- /dev/null +++ b/examples/server/routes.js @@ -0,0 +1,31 @@ +import React from 'react' +import { Route, IndexRoute, Link } from 'react-router' + +const App = ({ children }) => ( +
+
+ Links: + {' '} + Home + {' '} + Foo + {' '} + Bar +
+ {children} +
+) + +const Home = () => (
Home!
) +const Foo = () => (
Foo!
) +const Bar = () => (
Bar!
) + +const routes = ( + + + + + +) + +export default routes diff --git a/examples/server/server.js b/examples/server/server.js new file mode 100644 index 0000000..9a4ecef --- /dev/null +++ b/examples/server/server.js @@ -0,0 +1,62 @@ +/*eslint-disable no-console */ +import express from 'express' +import serialize from 'serialize-javascript' + +import webpack from 'webpack' +import webpackDevMiddleware from 'webpack-dev-middleware' +import webpackConfig from './webpack.config' + +import React from 'react' +import { renderToString } from 'react-dom/server' +import { Provider } from 'react-redux' +import { createMemoryHistory, match, RouterContext } from 'react-router' +import { syncHistoryWithStore } from '../../src' + +import { configureStore } from './store' +import routes from './routes' + +const app = express() + +app.use(webpackDevMiddleware(webpack(webpackConfig), { + publicPath: '/__build__/', + stats: { + colors: true + } +})) + +const HTML = ({ content, store }) => ( + + +
+
+