Coder Social home page Coder Social logo

react-router-redux's Introduction

Project Deprecated

This project is no longer maintained. For your Redux <-> Router syncing needs with React Router 4+, please see one of these libraries instead:


⚠️ This repo is for react-router-redux 4.x, which is only compatible with react-router 2.x and 3.x

react-router-redux

npm version npm downloads build status

Keep your router in sync with application state

Formerly known as redux-simple-router

You're a smart person. You use Redux to manage your application state. You use React Router to do routing. All is good.

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.

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, 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.

This library is not necessary for using Redux together with React Router. You can use the two together just fine without any additional libraries. It is useful if you care about recording, persisting, and replaying user actions, using time travel. If you don't care about these features, just use Redux and React Router directly.

Installation

npm install --save react-router-redux

How It Works

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.

history + store (redux) → react-router-redux → enhanced historyreact-router

Tutorial

Let's take a look at a simple example.

import React from 'react'
import ReactDOM from 'react-dom'
import { createStore, combineReducers } from 'redux'
import { Provider } from 'react-redux'
import { Router, Route, browserHistory } from 'react-router'
import { syncHistoryWithStore, routerReducer } from 'react-router-redux'

import reducers from '<project-path>/reducers'

// Add the reducer to your store on the `routing` key
const store = createStore(
  combineReducers({
    ...reducers,
    routing: routerReducer
  })
)

// Create an enhanced history that syncs navigation events with the store
const history = syncHistoryWithStore(browserHistory, store)

ReactDOM.render(
  <Provider store={store}>
    { /* Tell the Router to use our enhanced history */ }
    <Router history={history}>
      <Route path="/" component={App}>
        <Route path="foo" component={Foo}/>
        <Route path="bar" component={Bar}/>
      </Route>
    </Router>
  </Provider>,
  document.getElementById('mount')
)

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.

const history = syncHistoryWithStore(browserHistory, store)

history.listen(location => analyticsService.track(location.pathname))

For other kinds of events in your system, you can use middleware on your Redux store like normal to watch any action that is dispatched to the store.

What if I use Immutable.js or another state wrapper with my Redux store?

When using a wrapper for your store's state, such as Immutable.js, you will need to change two things from the standard setup:

  1. By default, the library expects to find the history state at state.routing. If your wrapper prevents accessing properties directly, or you want to put the routing state elsewhere, pass a selector function to access the historystate via the selectLocationState option on syncHistoryWithStore.
  2. Provide your own reducer function that will receive actions of type LOCATION_CHANGE and return the payload merged into the locationBeforeTransitions property of the routing state. For example, state.set("routing", {locationBeforeTransitions: action.payload}).

These two hooks will allow you to store the state that this library uses in whatever format or wrapper you would like.

How do I access router state in a container component?

React Router provides route information via a route component's props. This makes it easy to access them from a container component. When using react-redux to connect() your components to state, you can access the router's props from the 2nd argument of mapStateToProps:

function mapStateToProps(state, ownProps) {
  return {
    id: ownProps.params.id,
    filter: ownProps.location.query.filter
  };
}

You should not read the location state directly from the Redux store. This is because React Router operates asynchronously (to handle things such as dynamically-loaded components) and your component tree may not yet be updated in sync with your Redux state. You should rely on the props passed by React Router, as they are only updated after it has processed all asynchronous code.

What if I want to issue navigation events via Redux actions?

React Router provides singleton versions of history (browserHistory and hashHistory) that you can import and use from anywhere in your application. However, if you prefer Redux style actions, the library also provides a set of action creators and a middleware to capture them and redirect them to your history instance.

import { createStore, combineReducers, applyMiddleware } from 'redux';
import { routerMiddleware, push } from 'react-router-redux'

// Apply the middleware to the store
const middleware = routerMiddleware(browserHistory)
const store = createStore(
  reducers,
  applyMiddleware(middleware)
)

// Dispatch from anywhere like normal.
store.dispatch(push('/foo'))

Examples

Examples from the community:

Have an example to add? Send us a PR!

API

routerReducer()

You must add this reducer to your store for syncing to work.

A reducer function that stores location updates from history. If you use combineReducers, it should be nested under the routing key.

history = syncHistoryWithStore(history, store, [options])

Creates an enhanced history from the provided history. This history changes history.listen to pass all location updates through the provided store first. This ensures if the store is updated either from a navigation event or from a time travel action, such as a replay, the listeners of the enhanced history will stay in sync.

You must provide the enhanced history to your <Router> component. This ensures your routes stay in sync with your location and your store at the same time.

The options object takes in the following optional keys:

  • selectLocationState - (default state => state.routing) A selector function to obtain the history state from your store. Useful when not using the provided routerReducer to store history state. Allows you to use wrappers, such as Immutable.js.
  • adjustUrlOnReplay - (default true) When false, the URL will not be kept in sync during time travel. This is useful when using persistState from Redux DevTools and not wanting to maintain the URL state when restoring state.

push(location), replace(location), go(number), goBack(), goForward()

You must install routerMiddleware for these action creators to work.

Action creators that correspond with the history methods of the same name. For reference they are defined as follows:

  • push - Pushes a new location to history, becoming the current location.
  • replace - Replaces the current location in history.
  • go - Moves backwards or forwards a relative number of locations in history.
  • goForward - Moves forward one location. Equivalent to go(1)
  • goBack - Moves backwards one location. Equivalent to go(-1)

Both push and replace take in a location descriptor, which can be an object describing the URL or a plain string URL.

These action creators are also available in one single object as routerActions, which can be used as a convenience when using Redux's bindActionCreators().

routerMiddleware(history)

A middleware you can apply to your Redux store to capture dispatched actions created by the action creators. It will redirect those actions to the provided history instance.

LOCATION_CHANGE

An action type that you can listen for in your reducers to be notified of route updates. Fires after any changes to history.

react-router-redux's People

Contributors

barrystaes avatar chentsulin avatar choonkending avatar ctumolosus avatar decafdennis avatar dlmr avatar ellbee avatar freeqaz avatar geowarin avatar glennr avatar grifotv avatar jlongster avatar justingreenberg avatar kimjoar avatar kolodny avatar kuy avatar langpavel avatar nschurmann avatar pnavarrc avatar riccardozaffalon avatar rickcarlino avatar robertfall avatar simenb avatar svnm avatar taion avatar timdorr avatar tomatau avatar webmasterkai avatar wesbos avatar wyze avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

react-router-redux's Issues

0.0.10 breaks devtools

I originally filed this issue on freeqaz/redux-simple-router-example#1. I am able to reproduce the behavior elsewhere by switching between 0.0.8 and 0.0.10, so maybe this is a more appropriate place to file the issue?

Gif for your viewing pleasure:

The only change I see to the state as reported by devtools is the addition of changeId.

Can't use with reselect

We're trying to clean up our state management with reselect. Selectors act on redux state but some state lives in the URL so we need a way to sync react router state with redux so we can use router params in our selectors.

The problem with this implementation is we can't grab params or queries from state. There's no access to this.props from redux selectors. Any thoughts on how to fix that or will that never be possible with this library?

Back button doesn't work

Going back routes using the browser's back button does not work on the first try, without triggering any actions it takes 3 tries but when I trigger an action it requires a lot more to get back, e.g. 9 clicks of the back button when 2 actions triggered and 14 again when 4 actions triggered.

If it is useful, I am using react-router's <Link /> component to fashion my links.

Add react-router data into action and reducer

Add react router data into action and reducer

components: Array[],
location: Object,
params: Object,
routes: Array[]

I make a commit, just to show my Idea

https://github.com/vladimirbuskin/redux-simple-router/commit/0c4240ba63efe1454fdb0b762b109a710c4e03f5

Can we add something like that into simple router, because i think it is significant.
Data is great in actions to use in middlewares, and usefull in state as well. For example will help to use router in
react-redux-universal-hot-example

what do you think?

Need more info in state.routing

Maybe redux-router is a better choice for me because I need a little mas information in state but i'd like to know which route component is currently matched and the params and query params for that route. Redux router also provides me an array of parent routes which is helpful for creating breadcrumb navigations. Do you know if I can still get this kind of information from react-router directly since it's not currently in state?

Is it compatible with react-router <Link>?

When I do dispatch(updatePath('/foo')) all works great.
But, we have react-router <Link> components and they are broken now.
Can redux-simple-router work with react-router <Link>s, or we need to call dispatch(updatePath(newPath)) for all those links now?

Examples of how to use react-router API directly

In the README you wrote We encourage direct access of react-router APIs.
Could you please give some examples of how to do it? For example, right now I have this simple fetch function as an onEnter hook:

const fetchData = (nextState) => {
  if (nextState.location.action !== 'POP') {
    const params = nextState.params;
    const components = nextState.routes.map(route => route.component);
    fetchComponentData(store.dispatch, components, params);
  }
};

Is it possible to make it a redux middleware? How would I access nextState (in particular I need a component associated with the next route and params object) and other arguments in case I want to build a blocking UI.

const { UPDATE_PATH } = require('redux-simple-router');

export default store => next => action => {
  const { type, ...rest } = action;

  if (type === UPDATE_PATH) {
    //perform a fetch
  }

  next(action);
};

IndexRedirect causes infinite route

If I use an IndexRedirect with a basename I seem to get an infinite route.
Filing it here because it only happens when I call syncReduxAndRouter.

Commenting out the basename seems to resolve the issue ( I assume because there is nothing to repeat).

If I hit /test, I get a infinite loop of /test/test/test/...... until the call stack is exceeded.

Gist I'm using to verify

Required propTypes warning

After using redux-simple-router, I get many warnings like "Warning: Failed propType: Required prop foo was not specified in FooComp. Check the render method of RoutingContext."

But when I checked the prop in the FooComp, it's already there.

It this a bug?

Support for relative path in updatePath

Do you have a plan for supporting relative path in updatePath?

When I use relative path in updatePath, I can see the URL change but get a warning: Warning: Location "foo" did not match any routes

1.0.0 breaks if history uses `basename` option

Hi,
We are currently using react-router version 1.0.0 createHistory which imports the browser history strategy.

With 0.0.10 all was fine, but upgrading causes a pushState to loop forever. :(

We are wiring up basename like this:

const history = useBasename(createHistory)({basename: '/foobar'});

Now, when we use pushState it loops. I think that the problem is in this code:

} 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 }))
    }

The !locationsAreEqual always returns true because the call to history.createPath(location) comes back with the basename prepended.

If I debug in the browser, I can see it looping over this case.

I think the solution would be to add this type of thing lastRoute.changeId !== routing.changeId like in unsubscribeStore.

I'll see if I can reproduce in a unit test and issue a PR.

Rename to react-router-redux

@ryanflorence and @mjackson have invited this project to join the rackt set of projects since it aligns well with how they envision react-router and redux integration. I think it will essentially replace redux-router and that will live as its own standalone project.

We're considering renaming it react-router-redux as it will be more official bindings. Unless anyone has any better ideas, I say we go with it.

Server-side rendering being discarded because of Redux DevTools

After better analysing the problem submitted in #31 I realised the issue is with how Redux DevTools is rendered on the server, where the redux-simple-router action's have not yet been dispatched.

 (client) c.1.1.1.0.1.$1.0.0">@@router/UPDATE_PATH
 (server) c.1.1.1.0.1.$1.0.0">STARGAZERS_REQUEST</

So basically STARGAZERS_REQUEST is the first action that is loaded on the server, while redux-simple-router's @@router/UPDATE_PATH is the first action loaded on the client. So server-side rendered is discarded on development because of ReduxDevTools, but works fine on production. But than we lose the ability to debug server-side rendering...

`routeState.path` is undefined in the npm-module

Hey, just a quick info:

I installed this via npm and found that there is an error in the npm package version of this.
Running npm run build will fix this, only the lib/index.js contains the error.

onEnter / onLeave functionality (access to state)?

I had some frustration with redux-router both from configuration point of view and from impemenentation of something like requireAuth() function checks, that used in onEnter() hooks to prevent access to autorized only application parts.

redux-router has an issue with that and I'm trying to understand, does it this also relate to redux-simple-router project.

Can state be accessed easily with this router?

When will the next release be?

I noticed that the master branch has been updated to export pushPath and replacePath instead of updatePath. The version number is the same as the current release though, and I was wonder how long it will be before the next version-increment/release.

Being able to replace the current path is a blocking feature for me.

PS: Great library! Even without replace I like it a lot better than react-router.

changelog

I saw the v0.0.10 update today but can't find any record of changes since the last version. Is there intentionally no changelog at this point? With the increasing number of projects that are relying on this, I'd say it's worth bumping to v0.1 or v1 and recording changes between releases ASAP. Hard to decide how to maintain this as a dependency otherwise.

Thanks for the great work, @jlongster!

Why store only path and not location with all the props?

First of all, thanks for compact and simple thing! Could you tell, what's the motivation beside storing only 'compacted' path string and not the whole location object? It would save users (yes, like me) from manually parsing url string for query params.

Using basename for history causes infinite loop

I want to run my app in a context so i need a history basename:

import {createHistory,useBasename} from 'history';
const history = useBasename(createHistory)({
    basename: '/mybase'
});

I also use an onEnter in the route which loads current session and calls dispatch. The dispatch runs into the syncReduxAndRouter and call pushState causing the loop. This happens because redux-simple-router uses window.location to check the changes and is not is not aware of the basename used in the history so it always calls pushState

replaceState support

Do you have opinions about adding support to replace state in addition to updating it? It seems like there could be parallel REPLACE_PATH, replacePath exports with internals that mimic the existing functions.

I'm more than happy to come up with a PR, just didn't want to step on any plans that were already in place.

As mentioned in this comment it seems like the history API might be changing as well, though that seems pretty easy to handle.

updatePath not changing the address or redirecting

I am using the following code :

return store.dispatch(updatePath('/'));

it dispatches properly and I see that the next State has '/' as the path but the address bar isn't changing and the page doesn't actually re-direct.

Implement in react-redux-universal-hot-example

There's some talk about this library in erikras/react-redux-universal-hot-example#439

Would you be interested in starting a branch to integrate this library as a replacement? It could serve as a good example on how to migrate from redux-router to redux-simple-router, and could uncover pain points along the way.

I'd be happy to help, too. But for the next few days, probably not much.

If you think it would not serve as a good replacement, that would be great information, too!

Invite kjbekkelund to be a collaborator

Hey @kjbekkelund, you've done a fantastic job not only writing code and researching a lot of ideas but also responding to other people's issues and PRs. It'd be great to give you commit access here so you can move things forward as you see fit. Of course I'll still be around too but feel free to merge stuff if you think it's ready.

Redux-simple-router and React-Router replaceState interactions

Hello! I need to implement authentication mechanics to my application and so instead of starting from scratch I've used React-Router auth-flow example as example.

I'm facing a strange problem: from the Login component (roughly the same as the example above) I should be able to redirect to the previous visited route if auth is successfull:

if (loggedIn) {
   const { location, history } = this.props;

   if (location.state && location.state.nextPathname) {
       history.replaceState(null, location.state.nextPathname);
   } else {
       history.replaceState(null, '/');
   }
}

This path should be stored inside the location state object by the onEnter hook on a protected route:

function requireAuth(nextState, replaceState) {
    if (! isLoggedIn()) {
        replaceState({ nextPathname: nextState.location.pathname }, '/login');
    }
}

If I use this logic with redux-simple-route, inside the Login component the props this.location.state is null, while, if disabled, contains the right nextPathname property.

I think I've narrowed down the problem to these lines of redux-simple-router (68-71):

if(lastChangeId !== routing.changeId) {
  lastChangeId = routing.changeId;
  history.pushState(null, routing.path);
}

It pushes the route to history (which it should be not necessary) and doesn't pass the state in the process.

A simple fix would be setting changeId in the initial state to 0 but I don't know if it may cause other things to stop working...

UpdatePath after API call

What is the best way to update the path after making an API call? Right now I am making my API call but it is getting to the component before the all the data has loaded.

This is how I am doing it now:

this.props.dispatch(getSpecAcct(e));
this.props.dispatch(updatePath('/comppage'));

compatible with HashHistory?

I see a strange behavior if I try to use this with HashHistory and call syncReduxAndRouter.

The queryKey param (http://rackt.org/history/stable/HashHistoryCaveats.html) seems to change every second or so, changing in the URL bar and I think it's reloading the route every second as well.

image

I get this warning in console:

Warning: You cannot PUSH the same path using hash history

It's possible this is something with my app, but I didn't see it when using redux-router.

I like this API much better! Just curious if anyone else has seen this behavior.

How to prevent side effects from triggering when replaying routing actions?

When replaying a sequence of routing actions (basically what this repo enables Redux DevTools to do), how do you prevent side effects (such as data fetching triggering additional dispatches) from happening? According to the React Router 1.0 docs (https://github.com/rackt/react-router/blob/master/docs/guides/advanced/ComponentLifecycle.md), component life cycle methods are a good place to trigger side effects, such as data fetching using the URL params. But won't that clobber the sequence of actions you're replaying? In other words, won't that break the idempotence of restoring the UI to its current state? If dispatching routing actions trigger additional dispatches through side effects, the sequence of actions will be interrupted by unexpected additional actions, which makes replaying the sequence unpredictable. How do I solve this? Or am I missing something here?

I asked more or less the same question in acdlite/redux-router#207 and remix-run/react-router#2045.

Updating docs & changelog

Hey all,

Can someone help update the docs to reflect the new API? We also need to start a changelog. If someone could also create a CHANGELOG.md file and describe the changes that have happened, that would be awesome.

I will release it once that is done. I have other work I need to do tonight, and I'll be at a work week all week this week so I won't have a whole lot of time.

Custom selector function for reducer state

Right now the docs say that the reducer state "must be named 'routing'".

I vote that we remove this requirement and let developers decide for themselves how they want to structure their app state. In fact, I currently use ImmutableJs Map objects for my state hierarchy (I wrote my own combineReducers function instead of the redux default), so I'm not even sure I can use this library as-is.

I wrote a similar PR for redux-router here, which changes the API to accept a "selector" function instead of just assuming getState().router. We can likely do something similar here.

plan to support state object.

history.pushState() takes a state object as first parameter and Link tag takes it as a property. It looks like redux-simple-router do not support this state object yet.
Do you have a plan to support this?

Server-side rendering being discarded

It's beautiful how simple it is to implement this. But I'm having some trouble with server-side rendering. I added the syncReduxAndRouter function where the client is initiated. But what should be done in the server? Since createHistory can't be used?

It's a little obvious but the markup from my server and my client are different, and my server-side rendering is being discarded. You can clone the redux-simple-router branch of my repo if you want to test it out.

git clone -b redux-simple-router https://github.com/luandro/hapi-universal-redux.git
npm i
npm run dev

syncReduxAndRouter on server side?

I've been looking through your example of porting over react-redux-universal-hot-example and see that you call syncReduxAndRouter on the client side, which works great. However, I have a component which needs to grab the current path in fetchData when it's called on the server side. Since we don't sync until we get to the client, the path isn't updated correctly when the server renders.

Should I just be firing initPath with the values from the server side before running through fetchData functions?

First render - unneccasary history.pushState

On first page render, if redux DebugPanel is displayed, the simple router calls history.pushState with the existing route. This means that Router.createElement is invoked twice on ReactRouter - which isn't necessary.

The redux DevTools or perhaps LogMonitor seems to trigger the store.subrscribe callback on an interval. Here, RSR is finding that lastChangeId !== routing.changeId; and so calling the new pushState.

It could be worth caching the routePath and checking for a difference with the current getState().routing.path before invoking updatePath, but of course, that wouldn't take into account any route state changes.

It may be more simple to initialise the lastChangeId to the same value as initialState.changeId - again, there's probably a use case in here somewhere that route.state updates on initial render wouldn't be taken into account...

Perhaps #50 could prevent the unnecessary pushState call on initial render.

Weird initial state when using hash history

Accessing my app at http://localhost/react-bliss/public/index.html

The initial state for routing is set to /react-bliss/public/index.html

The history listener then updates the path to '/'

However with Redux Persist the store is updated before the update path action sets the correct path of '/'

This triggers redux-simple-router's store subscribe function and pushs the initial state to the browser - so I get a browser URL of http://localhost/react-bliss/public/index.html#/react-bliss/public/index.html?_k=djhrwj

This seems a symptom of the initial state redux-simple-router is using not being the 'real' initial state with hash history.

Potential solutions:

  1. Detect hash history is being used (if possible) or set a config option to specify hash history - and then in locationToString return only the hash
  2. Allow locationToString to be over-ridden by a user supplied function in a config object

Use case:

Using redux-simple-router with redux-persist and accessing at a url with a non-hash path
i.e. localhost/myapp/index.html instead of myapp.localhost

In my case I need to be able to access at the first url because I am using this with cordova and cannot rewrite the URL

Update comment or change flag name

Here in comment you use
avoidRouterUpdate but in code it has a name noRouterUpdate.

To keep it in sync it's needed to update flag name or comment.

PS:
For me avoidRouterUpdate looks better than noRouterUpdate

Invariant violation with syncReduxAndRouter + inline click handler

EDIT Closing because I don't think this is specific to redux-simple-router

With syncReduxAndRouter, I am seeing the following error in the console:

Uncaught Error: Invariant Violation: findComponentRoot(..., .0.2.1): Unable to find element. This probably means the DOM was unexpectedly mutated (e.g., by the browser), usually due to forgetting a <tbody> when using tables, nesting tags like <form>, <p>, or <a>, or using non-SVG elements in an <svg> parent. Try inspecting the child nodes of the element with React ID ``.

Without syncReduxAndRouter, the issue does not occur.

Here is a reproducible test case.

  • To reproduce the error, open the console and select a user.

I did a little digging and I believe I have some idea of why we are getting an invariant violation.

  1. When the user selects an option, the selectUser action is dispatched.
  2. In redux-simple-router, the subscribe callback is invoked synchronously. Because the change ids do not match, pushState occurs.
  3. In react-router (<Router/>), the history listener is invoked synchronously. This results in a setState.
  4. In react-redux (@connect), the subscribe callback is invoked synchronously. This results in a second setState within the same cycle.
  5. For reasons unknown to me (this might be due to React internals), two setState calls within the same cycle cause React to incorrectly search for the above data-reactid (.0.2.1), resulting in an invariant violation because the element cannot be found. (This data-reactid corresponds to an element that should be omitted from the render tree given the current props and state.)

I was thinking the solution, if possible, would be to eliminate the double setState. I see the React developers have looked into this a bit in a different context, and it does not seem to be supported.

No integration with params

What is preventing this library from providing insight into params and providing the ability to update them with the push and replace actions? No support for route params is the one thing holding me back from trying to adopt this library.

Not all issues with back button are resolved

Consider the following routes structure

<Route path="/" component={Main}>
  <IndexRoute component={LandingPage}/>
  <Route component={App}>
    <Route path="product" component={ProductPage}/>      
  </Route>
</Route>

When I go from LandingPage to ProductPage (simply clicking <Link to="/product">) and then try to go back, using browser's back button, I have to press back button five times for this transition to happen.

Immutable state

All of my state is an Immutable.js map. How would I make this work with redux-simple-router, given that it's currently checking if getState().routing exists?

Support query params?

This morning I've been playing around with how we could support query params in redux-simple-router (just as a thought experiment).

In history.listen we receive a Location. With pushPath and replacePath we can change the API to accept LocationDescriptors. However, with no way of creating a LocationDescriptor object from a LocationDescriptor string, it's difficult to support query params.

Here's an example of the problem:

pushPath('/foo?a=b')
// store:
{ location: '/foo?a=b' }


pushPath({ pathname: '/foo', query: { a: 'b' } })
// store:
{ location: { pathname: '/foo', query: { a: 'b' } } }


history.push('/foo?a=b')
// store:
{ location: { pathname: '/foo', query: { a: 'b' } } }


<Link to='/foo?a=b'>
// store:
{ location: { pathname: '/foo', query: { a: 'b' } } }

The problem is of course that every time you pushPath you will get the exact LocationDescriptor you pass in into the store, but every time you history.push or <Link> you'll get an object. We then can't reliably support query params. (If you're very strict about always passing in a LocationDescriptorObject you would have query support, but that feels very fragile.)

We can of course use history.createLocation right now, but it's most likely no longer going to be public in history V2.

To support query params we need to be able to create a LocationDescriptor object both from a LocationDescriptor string and from a Location. We could then easily support query params, and we could also deepEqual check them without knowing about query or anything else that's actually part of the LocationDescriptor (which is how we stop cycles, #50). With a createLocationDescriptor (that always returns a LocationDescriptorObject) our code base would actually be simpler, and we would get query support for free.

Okey, so some potential solutions:

Keep the solution we have now

I.e. path and state in store, but change to accepting LocationDescriptor in push and replace. We don't support query params.

Verbatim LocationDescriptor in store

We have to make it very clear that this is either an object or a string. So it's most likely not very usable for people. We "support" query params, iff you're very strict about always passing in a LocationDescriptorObject. Fragile.

Always go through history "before" store

pushPath
-> UPDATE_PATH_INTERNAL action
-> store.subscribe callback
-> history.push
-> history.listen callback
-> UPDATE_PATH action
-> store.subscribe callback where we break cycle

I.e. we don't trigger UPDATE_PATH immediately, but only within the history.listen callback. We would then reliably have a LocationDescriptorObject in the store, and we could therefore reliably support query params.

This is of course a more complex solution.

Conclusion

It was interesting going through the different solutions, but with createLocation going away and no createLocationDescriptor on the horizon I think it's best to just keep our current solution. I don't think it's worth the added complexity.

Maybe someone else has ideas on how to solve this? Or maybe this is another case for keeping history.createLocation public? @jlongster @mjackson @taion @justingreenberg

(Hopefully I haven't overlooked something very obvious 🙈)

Stopping cycles

There are two flows to manipulate the browser history:

1. Update URL/history
   -> history callback
   -> dispatch push/replace action
   -> store callback
2. Dispatch push/replace action
   -> store callback
   -> update history
   -> history callback

In both cases we call both of our callbacks, and in both cases we need to make sure that the flow stops before we create a cycle. E.g. let's say we want to change the URL by dispatching an action. This action triggers a history update, and when handling this update we have to make sure we don't dispatch yet another time.

To solve both cases without relying on deep-equal I think we need to track two variables — one to stop each flow.

To handle case 1 we set { avoidRouterUpdate: true } in the history callback. As we don't increment the changeId we can stop the flow in the store callback by comparing it against the lastChangeId. By using changeId we also enable actions in history.listenBefore callbacks.

To handle case 2 we need to stop after we have updated the history based on a pushState or replaceState action. The problem is how to keep track of this update. In case 1 we decide to not increment the change id, thereby stopping the flow, but when updating the history I'm not sure how we can do the same. Right now we rely on comparing the current router state location with the new history location, but it would be nice to not need the deep-equal dependency in #38.

One option is to keep some kind of state in location.state in the same way as we have changeId in the store state, but I'm not sure if that would work (We don't have the same control over the history methods as we have over the pushPath and replacePath actions). Another option is to add another variable, e.g. isRoutingFromAction, which keeps track of whether or not the current route change was triggered by an action. Basically something like this:

let isRoutingFromAction = false;

const unsubscribeHistory = history.listen(location => {
  if (isRoutingFromAction) {
    // By handling this we have stopped the cycle, and
    // therefore need to reset the state.
    isRoutingFromAction = false;
    return;
  }

  // ...
});

const unsubscribeStore = store.subscribe(() => {
  const routing = getRouterState();
  if(lastChangeId === routing.changeId) return;

  isRoutingFromAction = true;

  // ...
});

With this change all tests still pass and my app still works, but as we don't run the tests in browsers I'm not entirely sure about potential edge cases (e.g. I don't use listenBefore at all in my app).

(I can create a PR with this if anyone wants to play around with it)

Maybe someone has other ideas about how to handle this?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.