Socket
Socket
Sign inDemoInstall

redux-simple-router

Package Overview
Dependencies
1
Maintainers
1
Versions
13
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 0.0.10 to 1.0.0

CHANGELOG.md

135

lib/index.js

@@ -1,7 +0,10 @@

'use strict';
"use strict";
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
// constants
var deepEqual = require('deep-equal');
// Constants
var INIT_PATH = "@@router/INIT_PATH";
var UPDATE_PATH = "@@router/UPDATE_PATH";

@@ -12,12 +15,50 @@ var SELECT_STATE = function SELECT_STATE(state) {

// Action creator
// Action creators
function updatePath(path, avoidRouterUpdate) {
function initPath(path, state) {
return {
type: INIT_PATH,
payload: {
path: path,
state: state,
replace: false,
avoidRouterUpdate: true
}
};
}
function pushPath(path, state) {
var _ref = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2];
var _ref$avoidRouterUpdat = _ref.avoidRouterUpdate;
var avoidRouterUpdate = _ref$avoidRouterUpdat === undefined ? false : _ref$avoidRouterUpdat;
return {
type: UPDATE_PATH,
path: path,
avoidRouterUpdate: !!avoidRouterUpdate
payload: {
path: path,
state: state,
replace: false,
avoidRouterUpdate: !!avoidRouterUpdate
}
};
}
function replacePath(path, state) {
var _ref2 = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2];
var _ref2$avoidRouterUpda = _ref2.avoidRouterUpdate;
var avoidRouterUpdate = _ref2$avoidRouterUpda === undefined ? false : _ref2$avoidRouterUpda;
return {
type: UPDATE_PATH,
payload: {
path: path,
state: state,
replace: true,
avoidRouterUpdate: !!avoidRouterUpdate
}
};
}
// Reducer

@@ -27,3 +68,5 @@

changeId: 1,
path: typeof window !== 'undefined' ? locationToString(window.location) : '/'
path: undefined,
state: undefined,
replace: false
};

@@ -33,8 +76,12 @@

var state = arguments.length <= 0 || arguments[0] === undefined ? initialState : arguments[0];
var action = arguments[1];
var _ref3 = arguments[1];
var type = _ref3.type;
var payload = _ref3.payload;
if (action.type === UPDATE_PATH) {
if (type === INIT_PATH || type === UPDATE_PATH) {
return _extends({}, state, {
path: action.path,
changeId: state.changeId + (action.avoidRouterUpdate ? 0 : 1)
path: payload.path,
changeId: state.changeId + (payload.avoidRouterUpdate ? 0 : 1),
state: payload.state,
replace: payload.replace
});

@@ -47,4 +94,4 @@ }

function locationToString(location) {
return location.pathname + location.search + location.hash;
function locationsAreEqual(a, b) {
return a != null && b != null && a.path === b.path && deepEqual(a.state, b.state);
}

@@ -58,4 +105,12 @@

};
var lastChangeId = 0;
// To properly handle store updates we need to track the last route.
// This route contains a `changeId` which is updated on every
// `pushPath` and `replacePath`. If this id changes we always
// trigger a history update. However, if the id does not change, we
// check if the location has changed, and if it is we trigger a
// history update. It's possible for this to happen when something
// reloads the entire app state such as redux devtools.
var lastRoute = undefined;
if (!getRouterState()) {

@@ -66,7 +121,35 @@ throw new Error("Cannot sync router: route state does not exist. Did you " + "install the routing reducer?");

var unsubscribeHistory = history.listen(function (location) {
var routePath = locationToString(location);
var route = {
path: history.createPath(location),
state: location.state
};
// Avoid dispatching an action if the store is already up-to-date
if (getRouterState().path !== routePath) {
store.dispatch(updatePath(routePath, { avoidRouterUpdate: true }));
if (!lastRoute) {
// `initialState` *should* represent the current location when
// the app loads, but we cannot get the current location when it
// is defined. What happens is `history.listen` is called
// immediately when it is registered, and it updates the app
// state with an UPDATE_PATH action. This causes problem when
// users are listening to UPDATE_PATH actions just for
// *changes*, and with redux devtools because "revert" will use
// `initialState` and it won't revert to the original URL.
// Instead, we specialize the first route notification and do
// different things based on it.
initialState = {
changeId: 1,
path: route.path,
state: route.state,
replace: false
};
// Also set `lastRoute` so that the store subscriber doesn't
// trigger an unnecessary `pushState` on load
lastRoute = initialState;
store.dispatch(initPath(route.path, route.state));
} else if (!locationsAreEqual(getRouterState(), route)) {
// The above check avoids dispatching an action if the store is
// already up-to-date
var method = location.action === 'REPLACE' ? replacePath : pushPath;
store.dispatch(method(route.path, route.state, { avoidRouterUpdate: true }));
}

@@ -78,8 +161,9 @@ });

// Only update the router once per `updatePath` call. This is
// indicated by the `changeId` state; when that number changes, we
// should call `pushState`.
if (lastChangeId !== routing.changeId) {
lastChangeId = routing.changeId;
history.pushState(null, routing.path);
// Only trigger history update if this is a new change or the
// location has changed.
if (lastRoute.changeId !== routing.changeId || !locationsAreEqual(lastRoute, routing)) {
lastRoute = routing;
var method = routing.replace ? 'replaceState' : 'pushState';
history[method](routing.state, routing.path);
}

@@ -96,5 +180,6 @@ });

UPDATE_PATH: UPDATE_PATH,
updatePath: updatePath,
pushPath: pushPath,
replacePath: replacePath,
syncReduxAndRouter: syncReduxAndRouter,
routeReducer: update
};
{
"name": "redux-simple-router",
"version": "0.0.10",
"version": "1.0.0",
"description": "Ruthlessly simple bindings to keep react-router and redux in sync",

@@ -15,4 +15,10 @@ "main": "lib/index",

"scripts": {
"build": "mkdir -p lib && babel ./src/index.js --plugins transform-object-assign --out-file ./lib/index.js",
"test": "mocha --compilers js:babel-core/register --recursive",
"build": "mkdir -p lib && babel ./src/index.js --out-file ./lib/index.js",
"test": "npm run test:node && npm run test:browser",
"test:node": "mocha --compilers js:babel-core/register --recursive ./test/node",
"test:browser": "karma start",
"test:cov": "npm run test:cov:browser && npm run test:cov:node && npm run test:cov:report",
"test:cov:node": "babel-node $(npm bin)/isparta cover $(npm bin)/_mocha report --dir ./coverage/node-coverage -- --recursive ./test/node",
"test:cov:browser": "COVERAGE=true karma start",
"test:cov:report": "$(npm bin)/istanbul report --dir ./coverage --include **/*coverage.json html text",
"prepublish": "npm run build"

@@ -32,2 +38,3 @@ },

"babel-core": "^6.2.1",
"babel-loader": "^6.2.0",
"babel-plugin-transform-object-assign": "^6.0.14",

@@ -37,5 +44,22 @@ "babel-preset-es2015": "^6.1.2",

"history": "^1.13.1",
"isparta": "^4.0.0",
"isparta-loader": "^2.0.0",
"karma": "^0.13.3",
"karma-chrome-launcher": "^0.2.0",
"karma-coverage": "^0.5.3",
"karma-firefox-launcher": "^0.1.7",
"karma-ie-launcher": "^0.2.0",
"karma-mocha": "^0.2.0",
"karma-mocha-reporter": "^1.0.4",
"karma-safari-launcher": "^0.1.1",
"karma-sourcemap-loader": "^0.3.5",
"karma-webpack": "^1.7.0",
"mocha": "^2.3.4",
"redux": "^3.0.4"
"redux": "^3.0.4",
"redux-devtools": "^2.1.5",
"webpack": "^1.12.9"
},
"dependencies": {
"deep-equal": "^1.0.1"
}
}

166

README.md

@@ -1,80 +0,64 @@

# redux-simple-router
[Redux](https://github.com/rackt/redux) is cool.
[react-router](https://github.com/rackt/react-router) is neat. 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.
[![npm version](https://img.shields.io/npm/v/redux-simple-router.svg?style=flat-square)](https://www.npmjs.com/package/redux-simple-router) [![npm downloads](https://img.shields.io/npm/dm/redux-simple-router.svg?style=flat-square)](https://www.npmjs.com/package/redux-simple-router)
react-router automatically maps the current URL to a path down your
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.
**Let react-router do all the work** :sparkles:
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.
[Redux](https://github.com/rackt/redux) is awesome. [React Router](https://github.com/rackt/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.
```js
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.
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.
```
npm install redux-simple-router
```
## Isn't there already redux-router?
View the [CHANGELOG](https://github.com/jlongster/redux-simple-router/blob/master/CHANGELOG.md) for recent changes.
[redux-router](https://github.com/rackt/redux-router) is another
project which solves the same problem. However, it's far more complex.
Just look at this code: the whole thing is only 68 lines of JS.
redux-router is much bigger and more complex.
Read the [API docs](#api) farther down this page.
That said, redux-router is a fine project and has features this
doesn't provide. Use it if you like it better.
##### _What about redux-router?_
The differences between this and redux-router:
[redux-router](https://github.com/rackt/redux-router) is another project which solves the same problem. However, it's far more complex. Take a quick look at [the code for this library](https://github.com/jlongster/redux-simple-router/blob/master/src/index.js)—it's extremely minimal. redux-router is much bigger and more complex.
* Much smaller and simpler. You don't need to learn another library on
top of everything else.
* We encourage direct access of react-router APIs. Need server-side
rendering, or something else advanced? Just read react-router's
docs.
* The piece of state is just a URL string, whereas redux-router stores
the full location object from react-router.
That said, redux-router is a fine project and has features this doesn't provide. Use it if you like it better.
This only depends on the `history.listen` function from react-router
and the `store.getState` and `store.subscribe` functions from redux.
It should be very future-proof with any versions of either libraries.
**Compared with redux-router:**
## How to Use
* Much smaller and simpler. You don't need to learn another library on top of everything else.
* We encourage direct access of react-router APIs. Need server-side rendering, or something else advanced? Just read react-router's docs.
* We only store current URL and state, whereas redux-router stores the entire location object from react-router.
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 and "connected" components. It's even
possible for a single component to be both if needed.
### Usage
This library provides a single point of synchronization: the
`routing.path` piece of state which is the current URL. You can read
it, and also change it with an action.
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 and "connected" components. It's even possible for a single component to be both if needed.
Here's some code:
[redux](https://github.com/rackt/redux) (`store.routing`) &nbsp;&harr;&nbsp; [**redux-simple-router**](https://github.com/jlongster/redux-simple-router) &nbsp;&harr;&nbsp; [history](https://github.com/rackt/history) (`history.location`) &nbsp;&harr;&nbsp; [react-router](https://github.com/rackt/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
Let's take a look at a simple example.
```js
const { createStore, combineReducers } = require('redux');
const { Provider } = require('react-redux');
const { Router, Route } = require('react-router');
const createBrowserHistory = require('history/lib/createBrowserHistory');
const { syncReduxAndRouter, routeReducer } = require('redux-simple-router');
const reducers = require('<project-path>/reducers');
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore, combineReducers } from 'redux'
import { Provider } from 'react-redux'
import { Router, Route } from 'react-router'
import { createHistory } from 'history'
import { syncReduxAndRouter, routeReducer } from 'redux-simple-router'
import reducers from '<project-path>/reducers'
const reducer = combineReducers(Object.assign({}, reducers, {
routing: routeReducer
}));
const store = createStore(reducer);
const history = createBrowserHistory();
}))
const store = createStore(reducer)
const history = createHistory()
syncReduxAndRouter(history, store);
syncReduxAndRouter(history, store)
React.render(
ReactDOM.render(
<Provider store={store}>

@@ -89,24 +73,19 @@ <Router history={history}>

document.getElementById('mount')
);
)
```
Now you can read from `state.routing.path` to get the URL. It's far
more likely that you want to change the URL more often, however. You
can use the `updatePath` action creator the we provide:
Now you can read from `state.routing.path` to get the URL. It's far more likely that you want to change the URL more often, however. You can use the `pushPath` action creator that we provide:
```js
const { updatePath } = require ('redux-simple-router');
import { pushPath } from 'redux-simple-router'
function MyComponent({ dispatch }) {
return <Button onClick={() => dispatch(updatePath('/foo'))}/>;
return <Button onClick={() => dispatch(pushPath('/foo'))}/>;
}
```
This will change the state, which will trigger a change in react-router.
This will change the state, which will trigger a change in react-router. Additionally, if you want to respond to the path update action, just handle the `UPDATE_PATH` constant that we provide:
Additionally, if you want to respond to the path update action, just
handle the `UPDATE_PATH` constant that we provide:
```js
const { UPDATE_PATH } = require('redux-simple-router');
import { UPDATE_PATH } from 'redux-simple-router'

@@ -121,37 +100,42 @@ function update(state, action) {

For more context, check out this [full example](https://github.com/freeqaz/redux-simple-router-example).
### Examples
## API
* [examples/basic](https://github.com/jlongster/redux-simple-router/blob/master/examples/basic) - basic reference implementation
### `syncReduxAndRouter(history, store, selectRouterState?)`
Examples from the community:
Call this with a react-router and a redux store instance to install
hooks that always keep both of them in sync. When one changes, so will
the other.
* [davezuko/react-redux-starter-kit](https://github.com/davezuko/react-redux-starter-kit) - popular redux starter kit
* **tip**: migrating from redux-router? use [this commit](https://github.com/davezuko/react-redux-starter-kit/commit/db66626ca8a02ecf030a3f7f5a669ac338fd5897) as a reference
* [freeqaz/redux-simple-router-example](https://github.com/freeqaz/redux-simple-router-example) - example implementation
Supply an optional function `selectRouterState` to customize where to
find the router state on your app state. It defaults to `state =>
state.routing`, so you would install the reducer under the name
"routing". Feel free to change this to whatever you like.
_Have an example to add? Send us a PR!_
### `routeReducer`
### API
A reducer function that keeps track of the router state. You need to
add this reducer to your app reducers when creating the store. **The
piece of state must be named `routing`** (unless you provide a custom
`selectRouterState` function).
#### `syncReduxAndRouter(history, store, selectRouterState?)`
### `UPDATE_PATH`
Call this with a react-router and a redux store instance to install hooks that always keep both of them in sync. When one changes, so will the other.
An action type that you can listen for in your reducers.
Supply an optional function `selectRouterState` to customize where to find the router state on your app state. It defaults to `state => state.routing`, so you would install the reducer under the name "routing". Feel free to change this to whatever you like.
### `updatePath(path, noRouterUpdate)`
#### `routeReducer`
An action creator that you can use to update the current URL. Just
pass it a string like `/foo/bar?param=5`.
A reducer function that keeps track of the router state. You must to add this reducer to your app reducers when creating the store. If you do not provide a custom `selectRouterState` function, the piece of state must be named `routing`.
The `noRouterUpdate`, if `true`, will stop react-router from reacting
to this and all future URL changes. Pass `false` to make it start
reacting again. This is useful if replaying snapshots while using the
`forceRefresh` option of the browser history which forces full
reloads. It's a rare edge case.
#### `UPDATE_PATH`
An action type that you can listen for in your reducers to be notified of route updates.
#### `pushPath(path, state, { avoidRouterUpdate = false } = {})`
An action creator that you can use to update the current URL and update the browser history. Just pass it a string like `/foo/bar?param=5` as the `path` argument.
You can optionally pass a state to this action creator to update the state of the current route.
The `avoidRouterUpdate`, if `true`, will stop react-router from reacting to this URL change. This is useful if replaying snapshots while using the `forceRefresh` option of the browser history which forces full reloads. It's a rare edge case.
#### `replacePath(path, state, { avoidRouterUpdate = false } = {})`
An action creator that you can use to replace the current URL without updating the browser history.
The `state` and the `avoidRouterUpdate` parameters work just like `pushPath`.

@@ -0,14 +1,44 @@

const deepEqual = require('deep-equal');
// constants
// Constants
const INIT_PATH = "@@router/INIT_PATH";
const UPDATE_PATH = "@@router/UPDATE_PATH";
const SELECT_STATE = state => state.routing;
// Action creator
// Action creators
function updatePath(path, avoidRouterUpdate) {
function initPath(path, state) {
return {
type: INIT_PATH,
payload: {
path: path,
state: state,
replace: false,
avoidRouterUpdate: true
}
};
}
function pushPath(path, state, { avoidRouterUpdate = false } = {}) {
return {
type: UPDATE_PATH,
path: path,
avoidRouterUpdate: !!avoidRouterUpdate
payload: {
path: path,
state: state,
replace: false,
avoidRouterUpdate: !!avoidRouterUpdate
}
};
}
function replacePath(path, state, { avoidRouterUpdate = false } = {}) {
return {
type: UPDATE_PATH,
payload: {
path: path,
state: state,
replace: true,
avoidRouterUpdate: !!avoidRouterUpdate
}
}

@@ -19,14 +49,16 @@ }

const initialState = {
let initialState = {
changeId: 1,
path: (typeof window !== 'undefined') ?
locationToString(window.location) :
'/'
path: undefined,
state: undefined,
replace: false
};
function update(state=initialState, action) {
if(action.type === UPDATE_PATH) {
function update(state=initialState, { type, payload }) {
if(type === INIT_PATH || type === UPDATE_PATH) {
return Object.assign({}, state, {
path: action.path,
changeId: state.changeId + (action.avoidRouterUpdate ? 0 : 1)
path: payload.path,
changeId: state.changeId + (payload.avoidRouterUpdate ? 0 : 1),
state: payload.state,
replace: payload.replace
});

@@ -39,4 +71,4 @@ }

function locationToString(location) {
return location.pathname + location.search + location.hash;
function locationsAreEqual(a, b) {
return a != null && b != null && a.path === b.path && deepEqual(a.state, b.state);
}

@@ -46,4 +78,12 @@

const getRouterState = () => selectRouterState(store.getState());
let lastChangeId = 0;
// To properly handle store updates we need to track the last route.
// This route contains a `changeId` which is updated on every
// `pushPath` and `replacePath`. If this id changes we always
// trigger a history update. However, if the id does not change, we
// check if the location has changed, and if it is we trigger a
// history update. It's possible for this to happen when something
// reloads the entire app state such as redux devtools.
let lastRoute = undefined;
if(!getRouterState()) {

@@ -57,7 +97,35 @@ throw new Error(

const unsubscribeHistory = history.listen(location => {
const routePath = locationToString(location);
const route = {
path: history.createPath(location),
state: location.state
};
// Avoid dispatching an action if the store is already up-to-date
if(getRouterState().path !== routePath) {
store.dispatch(updatePath(routePath, { avoidRouterUpdate: true }));
if (!lastRoute) {
// `initialState` *should* represent the current location when
// the app loads, but we cannot get the current location when it
// is defined. What happens is `history.listen` is called
// immediately when it is registered, and it updates the app
// state with an UPDATE_PATH action. This causes problem when
// users are listening to UPDATE_PATH actions just for
// *changes*, and with redux devtools because "revert" will use
// `initialState` and it won't revert to the original URL.
// Instead, we specialize the first route notification and do
// different things based on it.
initialState = {
changeId: 1,
path: route.path,
state: route.state,
replace: false
};
// Also set `lastRoute` so that the store subscriber doesn't
// trigger an unnecessary `pushState` on load
lastRoute = initialState;
store.dispatch(initPath(route.path, route.state));
} else if(!locationsAreEqual(getRouterState(), route)) {
// The above check avoids dispatching an action if the store is
// already up-to-date
const method = location.action === 'REPLACE' ? replacePath : pushPath;
store.dispatch(method(route.path, route.state, { avoidRouterUpdate: true }));
}

@@ -67,11 +135,14 @@ });

const unsubscribeStore = store.subscribe(() => {
const routing = getRouterState();
let routing = getRouterState();
// Only update the router once per `updatePath` call. This is
// indicated by the `changeId` state; when that number changes, we
// should call `pushState`.
if(lastChangeId !== routing.changeId) {
lastChangeId = routing.changeId;
history.pushState(null, routing.path);
// Only trigger history update if this is a new change or the
// location has changed.
if(lastRoute.changeId !== routing.changeId ||
!locationsAreEqual(lastRoute, routing)) {
lastRoute = routing;
const method = routing.replace ? 'replaceState' : 'pushState';
history[method](routing.state, routing.path);
}
});

@@ -87,5 +158,6 @@

UPDATE_PATH,
updatePath,
pushPath,
replacePath,
syncReduxAndRouter,
routeReducer: update
};
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Packages

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc