Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redux and routing #805

Closed
cappslock opened this issue Sep 25, 2015 · 17 comments
Closed

Redux and routing #805

cappslock opened this issue Sep 25, 2015 · 17 comments

Comments

@cappslock
Copy link

From what I can tell, react-router seems to be the expected tool to use for routing. I like react-router, but it feels to me that when integrated with redux (at least as implemented in the real world example) it effectively creates a second state which must be reasoned about in addition to the store. That is to say, the store is no longer the driving component of the UI.

Independent of react-router, when thinking about routing in redux, I envisioned a page change being an action, a reducer updating the store with a location field, and a store subscriber handling the actual updating of the URL. The UI would then be able to render different components depending on the state of location in the store.

Is it already possible to achieve something like this workflow with react-router, possibly with a custom Link or history?

For that matter, do we even need react-router to achieve this? It seems like it may be possible to isolate some of the interesting parts of react-router to library functions invoked by reducers.

If I'm way off on this please point me in the right direction :)

@ronag
Copy link

ronag commented Sep 25, 2015

@cappslock
Copy link
Author

Thanks. I've seen that library, but it's not entirely clear to me if it implements something similar to what I'm describing or merely mirrors the location in the state. For example, if something else causes the location in the state to change, will the browser history update and the proper component then be rendered?

@gaearon
Copy link
Contributor

gaearon commented Sep 25, 2015

@acdlite Can you answer this?

@eugene1g
Copy link

I envisioned a page change being an action, a reducer updating the store with a location field, and a store subscriber handling the actual updating of the URL.

This is my approach as well, using react-router in combination with redux-router. It works like this -

  • react-router provides an overall wrapper - it uses the current location concept and renders a relevant holding component onto the screen.
  • The location concept above is implemented by https://github.com/rackt/history and behind the scenes can implement the location idea either as a hash-based URL or a full location address.
  • redux-router references the same history component and pulls location-related information into an isolated reducer called router. So throughout the Redux application tree, I can access state.router.location to pull out query parameters as needed.
  • In practice, I do not want to burden my simple components with location-specific information, so I extract location pieces (e.g. query parameters) during the the Redux' select phase, and inject them as props into the component.

As an example, I'm injecting router/URL information like this -

const mapAppStateToProps = state => {
    const urlQuery = state.router.location.query;
    return {
        groupByField: urlQuery.groupBy
    }
};
export default connect(mapAppStateToProps)(SummaryList);

This cover read-only interactions - I get current URL information as part of the application state, and I’m free to reference/inject it as I wish.

if something else causes the location in the state to change, will the browser history update and the proper component then be rendered

React-router and redux-router both use https://github.com/rackt/history for listening to location-related changes. Those hooks plug into native browser events, so anything that changes the location on the browser will also change the application state as a consequence. So you can change the URL manually, or call pushState() from JS completely outside of the React/Redux/Router universe, and the changes will propogate into the react-based state.

I envisioned a page change being an action...

This is the second part of the challenge - making “write” changes to the location. As I mentioned, these routing solution simply isten to native brwoser events, so calling those events yourself will be equivalent to making a routing change. Redux-router provides hooks for calling pushState/replaceState in the browser, and it’s possible to add a thin layer of abstraction on top of those hooks to use them as you want.

My application behaviour is driven almost entirely by the URL. If I need to change the grouping of items on the page, I’d trigger that by change the URL parameter of groupBy. In my case it made sense to add a small Redux middleware that internally uses redux-router to upate the current url. This way I can throw a simple action like {type: ‘URL_UPDATE_QUERY’, query: { groupBy: ‘country’ } } which would take care of the specifics.

Some sample code might be useful -

Redux middleware that provides an action to manipulate the current URL -

/**
 * This middleware is responsible for updating the page URL and change query parameters
 * Behind the scenes, it currently uses redux-router to actually change URL information
*/
import {replaceState,setState, pushState} from 'redux-router'

const supportedActions = ['URL_UPDATE_QUERY'];

/**
 * This actions will update the query parameter in the current URL. It accepts a single parameter, the 'queryParams' object, which will be used to patch the current URL query string. New fields will be merged with fields currently present in the URL query.
 */
export function actionUpdateURLQuery(queryParams) {
    return {
        type:  'URL_UPDATE_QUERY',
        query: queryParams || {}
    }
}

export default function urlHandlingMiddlware({ getState }) {

    return next => action => {
        if (typeof action !== 'object' || !supportedActions.includes(action.type)) {
            return next(action);
        }


        if (action.type == 'URL_UPDATE_QUERY') {

            const currentLocation = getState().router.location,
                  url             = currentLocation.pathname,
                  newQueryParts   = {...currentLocation.query, ...action.query};

            let newQuery = {};
            //Filter out empty and null values
            for (let queryKey in newQueryParts) {
                if (!String(newQueryParts[queryKey]).length) continue;
                newQuery[queryKey] = newQueryParts[queryKey];
            }
            next(replaceState(null, url, newQuery));
        }
    }
}

I can leverage this middleware by importing the actionCreator it provides, and calling the action any time I need to change the url/query params. For example, a SummaryList component has a grouping selector, and hence provides a onChangeGrouping handler, so I pass a handler that will simply update the current URL

import {actionUpdateURLQuery} from '../app/url-middleware.js';
import SummaryList from ‘./summarylist.js';

const mapAppStateToProps = state => {
    const urlQuery = state.router.location.query;
    return {
        groupByField: urlQuery.groupBy,
       //...actually group records and inject them as other props
    }
};
const propActions = {
    //Here I'm using the provided actionCreator to just patch the query string on the current page in response to user-interaction within that component
    onChangeGrouping: newGroupingValue => actionUpdateURLQuery({groupBy: newGroupingValue})
};
export default connect(mapAppStateToProps, propActions)(SummaryList);

@wmertens
Copy link
Contributor

I made an example where I explore going one step further: consider the URL
bar as an always-present bound input. Changes to it can (but don't have to)
result in store changes, but every time the store changes the URL bar is
updated to the canonical URL for that store state. Your application is
oblivious to the URL bar, and instead renders from the store (although it
could still use the react-router routes for choosing components to render).

wmertens/redux-react-router@3fe33e1

Note that this is just implemented on top of redux-react-router.

To make this easy to use, there should be utility functions that help you
create url -> action and state -> url mappings and apply them in the
right places. I haven't come up with them yet.

Using this, you can e.g. make Stack Overflow-type URLs where the question
title is embedded in the URL but after the question key. (
http://yourapp/q/87634/i-like-pure-things). Then if the url gets cut short
but the key is intact, it still works. While you are creating question the
URL can already reflect the question title.

@eugene1g
Copy link

@wmertens I believe react-router is designed for unidirectional dataflow in mind - the router only responds to URL changes, but it's never the other way around. So for the scenario of "When component X is rendered, the URL should be Y", the canonical answer is to reverse the cause and effect and "First set the URL to Y, and react-router will render X for you in consequence".

Changes to [the url] can (but don't have to) result in store changes, but every time the store changes the URL bar is updated to the canonical URL for that store state.

As I understand this, to me this objective sounds like two-way binding between the app state and the URL. The current constraint imposed by react-router (the unidrectional dataflow) tries to create predictable workflows where UI = Router(URL). This simplicity is lost if we try to manage two sides of the equation: UI = Router(URL) and URL = DeriveRoute(UI). With such two-way binding, I expect there to be a higher chance of conflicts, race conditions, and increased complexity of potentially doing dirty-checking on both app state and the URL before and after each action.

@wmertens
Copy link
Contributor

In fact it is working exactly like an input field in redux. A change in
input results in onChange(), resulting in store change, resulting in render
with new value.

The history component should immediately revert the url after a change
comes in, that doesn't happen now.

@wmertens
Copy link
Contributor

Another cool consequence of binding the URL to the store is that if you
make a (e.g.) moveToView ActionCreator that first loads required data
before showing a View (aka Route), it won't update the URL until the
element shows. This is the behavior that browsers have for normal pages,
and it removes that confusing moment when the URL changed but the page
didn't.

@wmertens
Copy link
Contributor

The more I think about it, the more it seems that just using
https://github.com/rackt/history with some Redux glue would be sufficient
for using the URL bar as a bound input.

  • Configure history to dispatch an action on url change, and immediately cancel the navigation. The URL remains unchanged.
  • The action is created by a custom ActionCreator that converts the Location object (https://github.com/rackt/history/blob/master/docs/Location.md) to whatever actions fitting the app. Transition things can be done here.
  • The app reducer stores the state. E.g. currentView, currentMessageId, etc.
  • A store listener calls a custom pure stateToLocation() function (a selector, basically) to set the URL with. If the path changes, do pushState; for other changes use replaceState.
  • To get a href to somewhere, pass the desired app state to that stateToLocation function
  • Server rendering simply creates a Location for the desired URL. If the stateToLocation ends up returning a different path some time during data resolution, send a redirect.

So the developer would need to provide a locationToActions ActionCreator
and a stateToLocation selector. The rest of the application is pretty
oblivious to URLs.
Routing is done by the app that renders based on the state.

@tomkis
Copy link
Contributor

tomkis commented Sep 27, 2015

What's in my opinion fundamentally wrong with redux-router is the presence of pushState action itself. Treating actions as commands instead events mostly leads to wrong design of the application. I would prefer keeping the routing transition as the part of my application's domain. Router should be responsible just for mounting / un-mounting some nested hierarchy component tree which may (or may not) be reflecting URL mapping. Ideally, the action would be something like: PRODUCT_LIST_SELECTED which results in side effect of changing the URL triggering hashchange, which is mapped to dispatching of new action called ROUTE_CHANGED.

Action(PRODUCT_LIST_SELECTED) -> SideEffect(CHANGE_URI) or eventually SideEffect(API_CALL) -> onhashchange (or any alternative) -> Dispatches Action(ROUTE_HAS_CHANGED) -> Propagating currentRoute to appState -> rendering corresponding UI component

This IMO depends on #569

@johanneslumpe
Copy link
Contributor

@tomkis1 not fully what you are describing, but does https://github.com/johanneslumpe/redux-history-transitions come closer to the flow you want?

@eugene1g
Copy link

Ideally, the action would be something like: PRODUCT_LIST_SELECTED which results in side effect of changing the URL triggering hashchange, which is mapped to dispatching of new action called ROUTE_CHANGED.

@tomkis1 Agreed. This is how redux-router can operate, and how my example above also operates. I haven't heard of @johanneslumpe's library when I did the internal implementation, but his interpretation is the same so I'll migrate to use that lib shortly.

@acdlite
Copy link
Collaborator

acdlite commented Sep 28, 2015

Hi everyone, sorry it's taken me a few days to respond.

For example, if something else causes the location in the state to change, will the browser history update and the proper component then be rendered?

Yes, this was one of the original "acceptance criteria" I laid out for the project.

I would prefer keeping the routing transition as the part of my application's domain. Router should be responsible just for mounting / un-mounting some nested hierarchy component tree which may (or may not) be reflecting URL mapping.

I don't necessarily disagree. I know some other folks have been exploring this area recently. https://github.com/christianalfoni/addressbar treats the address bar like an <input>, which is a cool idea and conceptually seems to make sense.

Redux Router takes its approach to routing from React Router and is best understood as an official integration between those two projects. (For instance, much of the new stuff in the latest release like <RoutingContext> and useRouting() and even history were developed specifically with a Redux integration in mind.) It's original name was Redux React Router — I switched it to Redux Router because 1) very little of it is React specific, in fact, as React Router continues moving in a direction of decoupling the routing and rendering parts of the library, and 2) I didn't want someone else to grab the name and confuse everybody.

@cappslock
Copy link
Author

This thread got away from me quickly :)

Lots of interesting discussion beyond what I can reply to succinctly, but it's nice to know with a bit more clarify the design goals of redux-router and alternative approaches that people have explored.

For my part, I do very much like the idea of treating the browser location as simply another bound component. Thinking in this approach, it is rendered when the state changes (in the simplest case, the location is stored as a string in the state). It and other components can create actions that update the location, a router-like component can render different portions of the app depending on its value, etc. You could imagine a more sophisticated router which can parameterize the location and stuff.

@tomkis
Copy link
Contributor

tomkis commented Sep 29, 2015

@johanneslumpe yes, that's basically the approach I would go for

@gaearon
Copy link
Contributor

gaearon commented Oct 5, 2015

I'll close this thread.
When React Router and Redux Router both hit 1.0 we'll add a doc section on using them.
You can track this in #637.

@cappslock
Copy link
Author

Apologies for adding a comment to a closed thread, but I created a small routing solution based on some of the ideas discussed in this thread. You can see it here if you are interested: #637 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants