Code Monkey home page Code Monkey logo

redux-first-router's Introduction

Redux-First Router

Think of your app in terms of states, not routes or components. Connect your components and just dispatch Flux Standard Actions!

Version Min Node Version: 6 Downloads Build Status

Motivation

To be able to use Redux as is while keeping the address bar in sync. To define paths as actions, and handle path params and query strings as action payloads.

The address bar and Redux actions should be bi-directionally mapped, including via the browser's back/forward buttons. Dispatch an action and the address bar updates. Change the address, and an action is dispatched.

In addition, here are some obstacles Redux-First Router seeks to avoid:

  • Rendering from state that doesn't come from Redux
  • Dealing with the added complexity from having state outside of Redux
  • Cluttering components with route-related code
  • Large API surface areas of frameworks like react-router and next.js
  • Routing frameworks getting in the way of optimizing animations (such as when animations coincide with component updates).
  • Having to do route changes differently in order to support server-side rendering.

Usage

Install

yarn add redux-first-router

(A minimal <Link> component exists in the separate package redux-first-router-link.)

Quickstart

// configureStore.js
import { applyMiddleware, combineReducers, compose, createStore } from 'redux'
import { connectRoutes } from 'redux-first-router'

import page from './pageReducer'

const routesMap = {
  HOME: '/',
  USER: '/user/:id'
}

export default function configureStore(preloadedState) {
  const { reducer, middleware, enhancer } = connectRoutes(routesMap)

  const rootReducer = combineReducers({ page, location: reducer })
  const middlewares = applyMiddleware(middleware)
  const enhancers = compose(enhancer, middlewares)

  const store = createStore(rootReducer, preloadedState, enhancers)

  return { store }
}
// pageReducer.js
import { NOT_FOUND } from 'redux-first-router'

const components = {
  HOME: 'Home',
  USER: 'User',
  [NOT_FOUND]: 'NotFound'
}

export default (state = 'HOME', action = {}) => components[action.type] || state
// App.js
import React from 'react'
import { connect } from 'react-redux'

// Contains 'Home', 'User' and 'NotFound'
import * as components from './components';

const App = ({ page }) => {
  const Component = components[page]
  return <Component />
}

const mapStateToProps = ({ page }) => ({ page })

export default connect(mapStateToProps)(App)
// components.js
import React from 'react'
import { connect } from 'react-redux'

const Home = () => <h3>Home</h3>

const User = ({ userId }) => <h3>{`User ${userId}`}</h3>
const mapStateToProps = ({ location }) => ({
  userId: location.payload.id
})
const ConnectedUser = connect(mapStateToProps)(User)

const NotFound = () => <h3>404</h3>

export { Home, ConnectedUser as User, NotFound }

Documentation

Basics

Flow Chart

Redux First Router Flow Chart

connectRoutes is the primary "work" you will do to get Redux First Router going. It's all about creating and maintaining a pairing of action types and dynamic express style route paths. If you use our <Link /> component and pass an action as its href prop, you can change the URLs you use here any time without having to change your application code.

Besides the simple option of matching a literal path, all matching capabilities of the path-to-regexp package we use are now supported, except unnamed parameters.

One of the goals of Redux First Router is to NOT alter your actions and be 100% flux standard action-compliant. That allows for automatic support for packages such as redux-actions.

The location reducer primarily maintains the state of the current pathname and action dispatched (type + payload). That's its core mission.

A minimal link component exists in the separate package redux-first-router-link.

Queries can be dispatched by assigning a query object containing key/vals to an action, its payload object or its meta object.

Redux First Router has been thought up from the ground up with React Native (and server environments) in mind. They both make use of the history package's createMemoryHistory. In coordination, we are able to present you with a first-rate developer experience when it comes to URL-handling on native. We hope you come away feeling: "this is what I've been waiting for."

Advanced

Sometimes you may want to dynamically add routes to routesMap, for example so that you can codesplit routesMap. You can do this using the addRoutes function.

Sometimes you may want to block navigation away from the current route, for example to prompt the user to save their changes.

Complete Scroll restoration and hash #links handling is addressed primarily by one of our companion packages: redux-first-router-restore-scroll (we like to save you the bytes sent to clients if you don't need it).

Ok, this is the biggest example here, but given what it does, we think it's extremely concise and sensible.

The following are features you should avoid unless you have a reason that makes sense to use them. These features revolve around the history package's API. They make the most sense in React Native--for things like back button handling.

Below are some additional methods we export. The target user is package authors. Application developers will rarely need this.

In earlier versions history was a peerDependency, this is no longer the case since version 2 has its own history management tool. This means that the arguments passed to connectRoutes(documentation) need to be changed.

You might run into a situation where you want to trigger a redirect as soon as possible in case some particular piece of state is or is not set. A possible use case could be persisting checkout state, e.g. checkoutSteps.step1Completed.

These packages attempt in similar ways to reconcile the browser history with redux actions and state.

Recipes

Help add more recipes for these use cases. PR's welcome!

Topics for things you can do with redux-first-router but need examples written:

  • Performing redirects bases on state and payload.
  • Use hash-based routes/history (see the migration instructions)
  • Restoring scroll position
  • Handling optional URL fragments and query strings
  • Route change pre- & post-processing
  • Code-splitting
  • Server-side rendering
  • Usage together with react-universal-component, babel-plugin-universal-import, webpack-flush-chunks.

Where is new feature development occuring?

Feature development efforts are occuring in the Respond framework Rudy repository.

Contributing

We use commitizen, run npm run cm to make commits. A command-line form will appear, requiring you answer a few questions to automatically produce a nicely formatted commit. Releases, semantic version numbers, tags, changelogs and publishing will automatically be handled based on these commits thanks to [semantic-release](https:/ /github.com/semantic-release/semantic-release).

Community And Related Projects

redux-first-router's People

Contributors

cdoublev avatar chapati23 avatar davidjnelson-bluescape avatar dependabot[bot] avatar faceyspacey avatar gouegd avatar greenkeeper[bot] avatar guillaumecisco avatar hedgepigdaniel avatar iansinnott avatar magicien avatar maksimdegtyarev avatar mictian avatar mikeverf avatar miriam-z avatar mkamakura avatar mpontus avatar mrloh avatar mungojam avatar nilos avatar olivier-kickstand avatar raganw avatar rayhatfield avatar re6exp avatar scriptedalchemy avatar sdougbrown avatar sergio-toro avatar shibe97 avatar toerndev avatar valbrand 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

redux-first-router's Issues

fromPath and toPath are not being called when param is number like string/number

Hi @faceyspacey,

the fromPath function is not being called when pathSegment looks like a number string.

const routesMap = {
	EVENT: {
		path: '/event/:eventId/',
		fromPath: (pathSegment, key) => {
			console.warn(pathSegment, key);
			return pathSegment;
		},
		toPath: (value, key) => {
			console.warn(value, key);
			return value;
		},
	},
};

When matching path like /event/3/, fromPath function is not called and eventId is automatically converted to number, and there is currently no way how to prevent this (as far as I know).

Similarly, toPath function is not being called, when a number is passed in.

I believe, that this is not the right behaviour. When I define fromPath/toPath, I want to have a full control over the process creating value from pathSegment and vice versa.

I think, that if fromPath/toPath are defined, only these should be called skipping auto convert mechanism (here https://github.com/faceyspacey/redux-first-router/blob/master/src/pure-utils/pathToAction.js#L45, and https://github.com/faceyspacey/redux-first-router/blob/master/src/pure-utils/actionToPath.js#L30, it should check if fromPath/toPath is defined as a first step, if then call it and return).

Additionally, it may be useful to add an option to enable/disable the number auto conversion when matching. Also I have found no mention about auto conversion of numbers in the docs, it might be good to add it there.

What do you think? Could we improve the implementation of fromPath/toPath as I wrote above?

Lastly, I would like to thank you for your library. It is really great! It saves me a lot of headaches. The idea of using actions this way without having to worry about url in the app is just amazing!

Thanks for your answer.

little api improvement: let enhancer wire up reducer and middleware

@faceyspacey Hi, I'm not actually using your package but I'm implementing something similar myself (you did a great job regardless). When browsing through your docs and code, I think I found some kind of oddity: RFR is using a store enhancer, but you require the user to hook up the reducer and middleware themselves. You could refactor the enhancer in a way that it automatically wires the reducer and middleware to the store. (L406) That would make setting up even more convenient. (Or is this a deliberate design descision?)

I know, it's nitpicking, but I thought you'd want to know. :)

Using simple <a> tags while abstracting away path structure

Hi, this looks like a great library and I'm excited to use it. I find it very appealing to work with objects like

{ type: 'GOTO_USER_PROFILE', payload: { userId: 1925724 } }

rather than specific paths. However, I want to keep my components very dumb and ignorant of actions; ideally the url prop passed into the component should be just a simple string that the component can render as e.g. <a href={props.url}>User Profile</a>. I want it to be the selectors' job to turn actions into urls for rendering. Is there function I can call to do that conversion without having to use the Link component?

Specifying a basename when app isn't hosted at site root

I'm trying to use redux-first-router in a create-react-app app that specifies a homepage in package.json since the app won't be hosted at the root of the site. From what I can tell, it looks like I'll have to include that route prefix for each entry in my routesMap. I've been looking through the RFR source to see if there's a way to pass a basename (or similar) option that would allow the pathname to be enhanced if that option exists but it doesn't look like it's possible at the moment.

Is there currently a way to use RFR when an app isn't going to run at the root of a site? If not, is it reasonable to request the ability to specify a basename - perhaps in the options passed to connectRoutes?

Most idiomatic way to group routes

Hi,

I came across the redux first router and it looks really good. Maybe the first really redux-first designed router i've seen!

You have made decision to not to have nested routes. And when looking at the shape of the route object, it indeed makes sense: It does not make sense to i.e dispatch multiple different route-type actions in case of nested routes. But it might make sense to call different thunks.

Anyhow it makes certain approaches difficult. Let's say i have wizard where i can land on every page (wizard_start, wizard_middle, wizard_end). Therefore i need to initialize same data for each page. This can be achieved by having same thunk for all of the pages. But it does not solve the problem completely: If i land to wizard_start, and move to wizard_middle, the thunk is dispatched again. That's something i don't want: I want my wizard pages to be grouped so that the thunk is dispatched only once, no matter which page i land first.

is there some idiomatic way in redux-first-router to achieve this currently? What i'm actually asking for is functionality for grouping routes together. Something like this:

const routes = [
  {
    routes: { //grouped wizard routes
      START: {url: '/wizard/start'},
      MIDDLE: {url: '/wizard/middle'},
      MIDDLE_BRANCH: {
        url: '/wizard/middle/:param',
        thunk: (dispatch, getState) => {
          // some routes within group could have custom logic for initializing
          // in addition to common thunk
          dispatch(loadBranchSpecificData(getState().route.param))
        }
      },
      END: {url: '/wizard/end'}
    },
    thunk: (dispatch, getState) => {
      // this would be called only once when you land on any route within group
      dispatch(initializeWizard())
    }
  },
  {
    type: 'SOME_OTHER_ROUTE',
    thunk: (dispatch, getState) => {
    }
  }
]

Usage with redux-persist

How would I use this with redux-persist?

  // routesMap.js
  โ€ฆ
  WALLET_SELECTOR: {
    path: '/wallet-selector/:currency',
    thunk: (dispatch, getState) => {

       // I don't seem to have the rehydrated state here.
       // Is it somehow possible to 'await' rehydration here?
      const { steps } = getState()

      canContinueIf(steps.currencySelected, dispatch)
    }
  },
  โ€ฆ

An in-range update of eslint-plugin-import is breaking the build ๐Ÿšจ

Version 2.4.0 of eslint-plugin-import just got published.

Branch Build failing ๐Ÿšจ
Dependency eslint-plugin-import
Current Version 2.3.0
Type devDependency

This version is covered by your current version range and after updating it in your project the build failed.

As eslint-plugin-import is โ€œonlyโ€ a devDependency of this project it might not break production or downstream projects, but โ€œonlyโ€ your build or test tools โ€“ preventing new deploys or publishes.

I recommend you give this issue a high priority. Iโ€™m sure you can resolve this ๐Ÿ’ช

Status Details
  • โŒ continuous-integration/travis-ci/push The Travis CI build could not complete due to an error Details

Commits

The new version differs by 10 commits.

  • 44ca158 update utils changelog
  • a3728d7 bump eslint-module-utils to v2.1.0
  • 3e29169 bump v2.4.0
  • ea9c92c Merge pull request #737 from kevin940726/master
  • 8f9b403 fix typos, enforce type of array of strings in allow option
  • 95315e0 update CHANGELOG.md
  • 28e1623 eslint-module-utils: filePath in parserOptions (#840)
  • 2f690b4 update CI to build on Node 6+7 (#846)
  • 7d41745 write doc, add two more tests
  • dedfb11 add allow glob for rule no-unassigned-import, fix #671

See the full diff

Not sure how things should work exactly?

There is a collection of frequently asked questions and of course you may always ask my humans.


Your Greenkeeper Bot ๐ŸŒด

Non-React Compatibility

Would it be possible/feasible to integrate this router with frameworks/libraries other than React?

Example with Create-React-Native-App

Sorry if this is not the right place for this discussion, but I'm having problems configuring redux-first-router with a CRNA (Create-react-native-app). Does anybody have a good example?

location prev and redirect

Hello, I was trying to achieve a common use case where an user wants to access a specific url /need_to_be_logged when his session has expired so it will be redirected to / with a login page (or a /login view). But after being logged, the user will be redirected to the previous url he wanted to see, not the default HOME . For that we need to keep in memory the url before being redirected to login.

I've thought the prev parameter in location will be updated after a redirect, example:

// this code redirects to LOGIN (which has a login component if the user is no more authenticated)
onBeforeChange: (dispatch, getState, action) => {
        const {user, location} = getState();
        if (action.type !== 'LOGIN' && user && !user.authenticated) {
            dispatch(redirect({type: 'LOGIN'}));
        }
    },

So when the user clicks on the button login for authenticate himself, it should be redirected to the need_to_be_logged view (the previous in memory) and not the default HOME. Of course we would like to use dispatch(redirect(state.location.prev)).
So I thought the state.location.prev will give me the prev location to redirect to, but this object is empty. Is it normal?
I've seen the prev is linked to the action.meta.location.prev so I've tweaked my code as:

import {redirect} from 'redux-first-router';
import {clone} from 'lodash';

export default {
    onBeforeChange: (dispatch, getState, action) => {
        const {user, location} = getState();
        const a = clone(action);

        if (action.type !== 'HOME' && user && !user.authenticated && a) {
            const action = redirect({
                type: 'LOGIN',
                meta: {
                    ...a.meta,
                    location: {
                        ...a.meta.location,
                        current: {
                            pathname: '/',
                            type: 'LOGIN',
                            payload: {},
                        },
                        prev: a.meta.location.current,
                    },
                },
            });
            dispatch(action);
        }
    },
};

And it works well, but it seems totally wrong to me and like a dirty hack.
Should not the redirect function change itself the prev attribute?

By the way, I had to clone the action object. Because if I don't, the action object becomes undefined at some point in the code. Something very very weird is happening on this object, like a mutability by another piece of code.

Map url with trailing slash to edit

Currently, with this routesMap:

COMPANY_LIST: { path: '/company', thunk: thunkList },
COMPANY_FORM: { path: '/company/:companyCode', thunk: thunkForm }, 

The url /company/ maps to COMPANY_LIST instead of to COMPANY_FORM.

Could we make /company/ maps to COMPANY_FORM instead?

Having used angular ui-router, it could facilitate this kind of url idiom for list, new, and edit:

/company 
/company/
/company/fb

Or we could keep /company and /company/ maps to same route, but could we have a way to configure that /company/ and /company/:companyCode maps to same route?

Authentication best practices with Redux First?

First off, this is great work -- thanks for making this! I'm so happy to have everything in Redux.

Could you provide some pointers/best practices on how authentication fits into this (examples would be amazing)? To be more specific in my case, I've been looking at this library for Amazon Cognito, which nicely holds user state in Redux: https://github.com/isotoma/react-cognito

With this library and React Router, one creates a guard

const guard = createGuard(store, '/forbidden');

and then passes it to onEnter in the Route to determine whether to show the user the route or redirect elsewhere if needed.

<Route
     path="/change_email"
     component={updateEmail}
     onEnter={guard()}
/>

What's the best way to do something like this/handle authentication and routing with Redux First?
Appreciate the help!

Render nested routes

Nice work with this, it is an interesting approach for routing. I have played around this evening but there are some areas I still have trouble figuring out.

I read the pre-release Medium post and that is what caused me to try it out. It makes a lot of sense when we have unique pages only. However if there are multiple levels of a path I am having trouble figuring out how to structure that.

For example let's say we have a books URL: /books, /books/:id and /books/:id/edit or something. For the first I want a sidebar to the left and content to right can be some statistics for all books. For the second I want the sidebar again and some statistics of a specific book as content. The last one also needs a sidebar and content is just some fields.

So in this example the three paths share some common sidebar module that we can extract and reuse. It feels a bit weird to code up three specific pages that shares a lot of the same layout code however. Also that we want some common data such as all books so we need to repeat thunk code as well.

What is a nice way to deal with this? I have tried three added route maps that all use the same BooksPage component. Then in that component I will do an additional {props.page === 'BOOKS_BOOK_EDIT_PAGE' && <BookEdit bookId={4] />} or something but that also kind of gets weird fast.

There might be a very obvious solution I am just not seeing, I hope there is. Any suggestions for how one might deal with this nested route with shared components/layout that does not result in too much redudant code that would not be with React Router?

I am a bit lost what to title this, sorry. Hope the question is more clear.

alternatives to thunk data requests

The docs say

... we recommend strategies that totally avoid thunks, such as Apollo's GraphQL client. Think of the thunk feature as a fallback or for simpler apps.

If you aren't using the GraphQL Client you are left thinking "I am doing something wrong" without any idea of what the alternatives are. I think more detail should be given about what recommended strategies avoid thunks, preferably with an example.

Clarify query string action to matching route map

I'm trying to add a query parameter to one of my routes, using:

<Link to={{type: 'SEARCH', payload: {find: this.state.findValue /* string */}}}> Blah </Link>

With the route

{
  SEARCH: '/search',
}

Ive also tried adding the query field, dased on these docs:

<Link to={{type: 'SEARCH', payload:  {find: this.state.findValue /* string */}}}> Blah </Link>

As well as adding a dynamic field to my route map:

{
  SEARCH: '/search/:find',
}

But I can't seem to get RFR to change my query string for me. I have to do something like

{
  SEARCH: '/search?find=:find
}

To get the query string to show up at all. But from your live example it seems that I shouldn't need to manually code the query string in the route. In addition, I never find my query at store.getState().location.query after I click my link.

So with that all said, what is the proper mapping between what you supply to Link to={...} and what your route looks like in your routes map, in order RFR create the query string for you?

SSR'd React components depending on query not matching client

When use in conjunction with SSR, it seems that when rendered on the server, the location state object doesn't container the query. So, a mapStateToProps like:

const mapStateToProps = ({location}) => {
  return {
    tabActive:
      get(location, ['query', 'find'], null) ===
      Constants.FIRST_OPTION,
  };
};

Gets the checksum react error for server markup not matching client markup, since on the server location.query is not present, yet on the client it is. Is there a proper way to access the query string from the server side rendering pass?

Update routesMap dynamically

I was recently working on a webapp for work and started messing around with per-client routing (it's a private webapp, SEO is not of concern). I currently have it implemented with slugs /:client/:thing but that was only after I tried wrapping the redux-first-router reducer to allow injecting new routes into the routesMap on the fly.

I was a bit disappointed to find that when I changed the routesMap in the redux store that change was not reflected by the middleware / enhancer. I had a brief poke through the code and it seems that this is due to the routesmap being passed into the enhancer when connectRoutes is called. I would assume it is possible for the enhancer to read the routesMap from the store instead.

So I suppose I am wondering if there would be any large issues if this was implemented. To my understanding it would change nothing in terms of the API (unless it had to be specifically enabled in the connectRoutes options). Certainly not a high priority feature but it would be nice to be able to define more solid routes for each client.

Usage with redux-saga

Hello,

would it be possible to delay action that is dispatched on initial load? I am using redux saga and the initial action is happening before sagas are started. It would be nice if connectRoutes would return a function or something similar so I can call it after sagas are started.

Route precedence

As routesMap is an object and the order of route matching depends solely on Object.keys return value. If I do something like:

const routesMap = { 
  HOME: '/home',
  USER_NEW: '/user/new',
  USER: '/user/:id',
}

There's absolutely no guarantee that the USER_NEW route has the precedence over the USER route.

It's possible to use regex in the matching:

const routeMaps = {
  HOME: '/home',
  USER_NEW: '/user/new',
  USER: /user/(((?!new).)*)$/
}

But that would cause the payload to be number indexed instead of named: {0: "123" }

Is this by design? This use case would definitely be benefited by some ordering mechanism in the routes. Either an order parameter or routesMap accepting an array instead of an object would be fine I believe.

Do you see this as a valid use case?

Does this enable sharing deeplinks?

If dependent on a Redux store/state to handle URLs, what happens when you try to share a deep link with a friend? How do they pick up the state to reflect the position in the app you shared?

Forgive me if this is a naive question...

Specifying an additional thunk argument

TL:DR: Would be cool if I could inject a third argument into thunks, like this:

const userThunk = async (dispatch, getState, api) => { // Notice the third argument
  const { slug } = getState().location.payload
  const data = await api.fetch(`/api/user/${slug}`) // Using the third argument
  const action = { type: 'USER_FOUND', payload: { user } }
  
  dispatch(action)
}

const routesMap = {
  USER: { path: '/user/:slug', thunk: userThunk  },
}

const api = {
  fetch(url) { /* Some custom logic... */ }
}

const { reducer, middleware, enhancer } = connectRoutes(history, routesMap, {
  extraThunkArgument: api
})

// etc...

Why

  • Consistency with redux-thunk. redux-thunk let's you inject a third argument
  • Improved thunk flexibility (this is also the reason redux-thunk supports it)

By improved flexibility I mean you get to do things like:

  • Mock API calls in tests (great for testing thunks)
  • Share singletons across thunks without importing them
  • Do other cool stuff I haven't though of...

For a quick example, let's say you are using Apollo Client without the react layer and you want to share the instance among thunks:

import ApolloClient, { createNetworkInterface } from 'apollo-client'
const networkInterface = createNetworkInterface({ uri: 'https://example.com/graphql' })
const client = new ApolloClient({ networkInterface })

// Set up thunk middleware
import thunk from 'redux-thunk';
const thunkMiddleware = thunk.withExtraArgument({ client });

// Set everything else up and create the redux store... 
// (you can imagine what this would look like)

So I'm proposing we allow the special RFR thunks to support the same functionality:

const { reducer, middleware, enhancer } = connectRoutes(history, routesMap, {
  extraThunkArgument: { client }
})

Conclusion

Let's do it? I'd be happy to PR this, but @faceyspacey I wanted to get your thoughts first.

onAfterChange doesn't wait for an asynchronous onBeforeChange to finish

Basically - I've got a check and fetch if missing situation, for some initial application info (like current user details), that I'd like to perform for any and all routes.

The way I was trying to achieve it up till now - to make a server call inside of the onBeforeChange function, but this attempt failed since onAfterChange doesn't wait for onBeforeChange to finish, and onAfterChange relies on what is being fetched previously.

Not sure if I'm barking up the wrong tree here - but is this intentional?

Recently switched over to this wonderful router - and have finally come to the first refactor effort after running with it for a while. Still considering where to place all the involved parts of My application in relation to the router, and this just came up, and if at all possible, wanted to hear Your opinion on this.

Immutable.JS support

Hi, thanks for an awesome library!

One thing that I've stumbled across when trying to use it in my app is a redux state wrapped in immutable structure. I'm using https://facebook.github.io/immutable-js across all my apps, so store.getState will return immutable map instead of a plain js object.

Is there any possibility to make it work?

Optional path parameters?

In redux-little-router one can specify optional parameters in route paths, e.g. /some/path/(:foo), and then both /some/path and /some/path/hello would map to the same route, in the former case passing { foo: undefined } as the parameters. Is there a similar mechanism in redux-first-router whereby those could both map to the same action but with an optional field in the payload?

[Question]: How can I address redirects efficiently with a more complicated scenario than the example

Firstly, I'm current using react/redux/react-router but I'm keen to drop react-router.
I've read your docs on redirecting but I'm not 100% sure how I can use it without repetition

Imagine I have the following routes (a greatly reduced list!):

/a
/a/a1
/a/a2
/b
/b/b1
/b/b2
/admin
/login

My app requires authenticated users so my top level route (/) checks for a user session and redirects to /login if the user isn't logged in. Simple to achieve in react-router

The user then needs a specific permission to access the nested routes e.g. /a, /b, /admin or they're redirected to /login

Now I could duplicate this logic for every distinct path in redux-first-router (using a helper function) but this will get tedious when the list of paths is long. Am I missing something?

[Discussion] Add a 'page' attribute to the RouteObject

It's fairly common that an app will want to load a particular "page" or scene for a given location type.

This is correctly pointed out in the Medium article where the following example is provided:

Hereโ€™s another kind of reducer youโ€™d likely have:

const page = (state, action) => {
  switch(action.type) {
    case 'HOME':
       return 'homeScene'
    case 'POST':
       return 'postScene'
    case 'REPOS':
      return 'reposScene'
    case 'REPO':
      return 'repoScene'
    case 'USER':
      return 'userScene'
  }
  
  return state
}

And its corresponding component:

const PageComponent = ({ page }) => {
  const Page = pages[page]
  return Page ? <Page /> : null
}
const mapState = ({ page }) => ({ page })
export default connect(mapState)(UserComponent)
// could be a switch (but this is cached):
const pages = {
  homeScene: HomeComponent, 
  postScene: PostComponent,
  reposScene: ReposComponent,
  repoScene: RepoComponent,
  userScene: UserComponent
}

What if we just added a page attribute to the RouteObject? Then we could have:

// routes.js

import HomePage from './HomePage.js'
import ContactPage from './ContactPage.js'

const routeMap = {
  HOME: { path: '/', page: HomePage },
  CONTACT: { path: '/', page: ContactPage }
}

and:

// PageComponent.js

import React from 'react'
import { connect } from 'react-redux'
import routes from './routes'

function mapStateToProps (state) {
  return {
    pathKey: state.location.type
  }
}

const PageComponent = ({ pathKey }) => {
  const Page = routes[pathKey].page
  return <Page />
}

export default connect(mapStateToProps)(PageComponent)

Thoughts?

RFR Dispatches previous route action

Hello,

I have found wierd behaviour with RFR. When I dispatch route action with meta.location.current.query = undefined current route action is dispatched and next action for route that I am trying to enter is dispatched.

Example where this behaviour is displayed https://codesandbox.io/s/gJjzq3O86. Watch in console what happens when you click on todo button.

Error swallowing prevention?

Trying this lib out, seems pretty useful thus far.

I have a question: using the universal component pattern, i.e.:

const UniversalComponent = universal(props => import(`./pages/${props.page}`), {
  chunkName: props => props.page,
  minDelay: 250,
  loading: () => (
    <div className='spinner'>
      <div />
    </div>
  ),
  error: () => (
    <NotFound />
  )
})

I get a lot of:

Uncaught (in promise) TypeError: Cannot read property '_currentElement' of null
    at ReactCompositeComponentWrapper._updateRenderedComponent (ReactCompositeComponent.js?063f:744)
    at ReactCompositeComponentWrapper._performComponentUpdate (ReactCompositeComponent.js?063f:723)
    at ReactCompositeComponentWrapper.updateComponent (ReactCompositeComponent.js?063f:644)
    at ReactCompositeComponentWrapper.receiveComponent (ReactCompositeComponent.js?063f:546)
    at Object.receiveComponent (ReactReconciler.js?c56c:124)
    at ReactCompositeComponentWrapper._updateRenderedComponent (ReactCompositeComponent.js?063f:753)
    at ReactCompositeComponentWrapper._performComponentUpdate (ReactCompositeComponent.js?063f:723)
    at ReactCompositeComponentWrapper.updateComponent (ReactCompositeComponent.js?063f:644)
    at ReactCompositeComponentWrapper.performUpdateIfNecessary (ReactCompositeComponent.js?063f:560)
    at Object.performUpdateIfNecessary (ReactReconciler.js?c56c:156)
    at runBatchedUpdates (ReactUpdates.js?be0d:150)
    at ReactReconcileTransaction.perform (Transaction.js?91bc:143)
    at ReactUpdatesFlushTransaction.perform (Transaction.js?91bc:143)
    at ReactUpdatesFlushTransaction.perform (ReactUpdates.js?be0d:89)
    at Object.flushBatchedUpdates (ReactUpdates.js?be0d:172)
    at ReactDefaultBatchingStrategyTransaction.closeAll (Transaction.js?91bc:209)
    at ReactDefaultBatchingStrategyTransaction.perform (Transaction.js?91bc:156)
    at Object.batchedUpdates (ReactDefaultBatchingStrategy.js?bdd7:62)
    at Object.enqueueUpdate (ReactUpdates.js?be0d:200)
    at enqueueUpdate (ReactUpdateQueue.js?959d:24)
    at Object.enqueueSetState (ReactUpdateQueue.js?959d:218)
    at UniversalComponent.ReactComponent.setState (ReactBaseClasses.js?3ba7:64)
    at UniversalComponent._this.update (index.js?3c89:87)
    at eval (index.js?3c89:128)
    at <anonymous>
_updateRenderedComponent @ ReactCompositeComponent.js?063f:744
_performComponentUpdate @ ReactCompositeComponent.js?063f:723
updateComponent @ ReactCompositeComponent.js?063f:644
receiveComponent @ ReactCompositeComponent.js?063f:546
receiveComponent @ ReactReconciler.js?c56c:124
_updateRenderedComponent @ ReactCompositeComponent.js?063f:753
_performComponentUpdate @ ReactCompositeComponent.js?063f:723
updateComponent @ ReactCompositeComponent.js?063f:644
performUpdateIfNecessary @ ReactCompositeComponent.js?063f:560
performUpdateIfNecessary @ ReactReconciler.js?c56c:156
runBatchedUpdates @ ReactUpdates.js?be0d:150
perform @ Transaction.js?91bc:143
perform @ Transaction.js?91bc:143
perform @ ReactUpdates.js?be0d:89
flushBatchedUpdates @ ReactUpdates.js?be0d:172
closeAll @ Transaction.js?91bc:209
perform @ Transaction.js?91bc:156
batchedUpdates @ ReactDefaultBatchingStrategy.js?bdd7:62
enqueueUpdate @ ReactUpdates.js?be0d:200
enqueueUpdate @ ReactUpdateQueue.js?959d:24
enqueueSetState @ ReactUpdateQueue.js?959d:218
ReactComponent.setState @ ReactBaseClasses.js?3ba7:64
UniversalComponent._this.update @ index.js?3c89:87
(anonymous) @ index.js?3c89:128

I get it a lot since I'm a React newbie and make a lot of mistakes with missing imports and connect-ing to null keys and so on. Is there a way to get better errors than just the same all the time? And sorry if this is the wrong place to ask!

Breaking change for v1.8.0

When moving from version 1.7.4 to 1.8.0 we are receiving this error which breaks on server-side rendering using webpack to build the server as well as the client

server:middleware:render ERROR processing request: TypeError: (0 , _reduxFirstRouter.locationKey) is not a function
  at Link (/{project dir}/node_modules/redux-first-router-link/dist/Link.js:66:69)
  at /{project dir}/build/main.node.js:23903:16
  at measureLifeCyclePerf (/{project dir}/build/main.node.js:23673:12)
  at ReactCompositeComponentWrapper._constructComponentWithoutOwner (/{project dir}/build/main.node.js:23902:14)
  at ReactCompositeComponentWrapper._constructComponent (/{project dir}/build/main.node.js:23877:21)
  at ReactCompositeComponentWrapper.mountComponent (/{project dir}/build/main.node.js:23785:21)
  at Object.mountComponent (/{project dir}/build/main.node.js:29680:35)
  at ReactCompositeComponentWrapper.performInitialMount (/{project dir}/build/main.node.js:23968:34)
  at ReactCompositeComponentWrapper.mountComponent (/{project dir}/build/main.node.js:23855:21)
  at Object.mountComponent (/{project dir}/build/main.node.js:29680:35)
  at ReactDOMComponent.mountChildren (/{project dir}/build/main.node.js:29006:44)
  at ReactDOMComponent._createContentMarkup (/{project dir}/build/main.node.js:25168:32)
  at ReactDOMComponent.mountComponent (/{project dir}/build/main.node.js:25035:29)
  at Object.mountComponent (/{project dir}/build/main.node.js:29680:35)
  at ReactDOMComponent.mountChildren (/{project dir}/build/main.node.js:29006:44)
  at ReactDOMComponent._createContentMarkup (/{project dir}/build/main.node.js:25168:32)

Using Koa 2 as the server.

Here is how our routes are configured

import { GO_HOME } from '../store/actions/home.types';
import {
  SEASON_STATS,
} from '../store/actions/stats.types';
import { setFilter } from '../store/actions/filter.actions';
import { getSeasonStats } from '../store/actions/stats.actions';

const routeConfig = {
  [GO_HOME]: '/',
  [SEASON_STATS]: {
    path: '/season',
    thunk: async dispatch => {
      const resetFilter = {};
      dispatch(setFilter(resetFilter));
      return dispatch(getSeasonStats);
    },
  },
};

export default routeConfig;

Here is how we are calling connectRoutes and creating the redux store in a koa middleware function

import createHistory from 'history/createMemoryHistory';
import { createStore, applyMiddleware, combineReducers, compose } from 'redux';
import { connectRoutes } from 'redux-first-router';
import thunkMiddleware from 'redux-thunk';
import inject from './middleware/inject';
import routeConfig from '../app/routes.global';
โ€ฆ
const routeOptions = {};  // TODO: implement options for managing private routes
โ€ฆ
async function addStore(ctx, next) {
  const history = createHistory({ initialEntries: [ctx.path] });
  const {
    reducer: routeReducer,
    middleware: routeMiddleware,
    enhancer: routeEnhancer,
    thunk: routeThunk,
  } = connectRoutes(history, routeConfig, routeOptions);

  const rootReducer = combineReducers({
    location: routeReducer,
  });
  const middlewares = [
    routeMiddleware,
    thunkMiddleware.withExtraArgument(inject),
  ];
  const appliedMiddleware = applyMiddleware(...middlewares);

  const enhancers = compose(
    routeEnhancer,
    appliedMiddleware,
  );

  const store = createStore(rootReducer, {}, enhancers);
  ctx.state.store = store;

Flow definitions incompatible with `react-redux`

After adding the flow definitions for react-redux, redux-first-router failed to check with the following errors:

node_modules/redux-first-router-link/dist/NavLink.js.flow:109
109: export default connector(NavLink)
                    ^^^^^^^^^^^^^^^^^^ function call. Function cannot be called on any member of intersection type
                                       v
 38:   declare type Connector<OP, P> = {
 39:     (component: StatelessComponent<P>): ConnectedComponentClass<OP, P, void, void>;
 40:     <Def, St>(component: Class<React$Component<Def, P, St>>): ConnectedComponentClass<OP, P, Def, St>;
 41:   };
       ^ intersection. See lib: flow-typed/npm/react-redux_v5.x.x.js:38
  Member 1:
   39:     (component: StatelessComponent<P>): ConnectedComponentClass<OP, P, void, void>;
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ function type. See lib: flow-typed/npm/react-redux_v5.x.x.js:39
  Error:
  106: const connector: Connector<OwnProps, Props> = connect(mapState)
                                            ^^^^^ intersection. This type is incompatible with
            v
   29:   }: {
   30:     to: To,
   31:     href?: To,
  ...:
   49:   },
         ^ object type
    Member 1:
                      v
     96: type Props = {
     97:   dispatch: Function, // eslint-disable-line flowtype/no-weak-types
     98:   pathname: string
     99: } & OwnProps
         ^ object type
    Error:
              v
     29:   }: {
     30:     to: To,
     31:     href?: To,
    ...:
     49:   },
           ^ property `isActive`. Property not found in
                      v
     96: type Props = {
     97:   dispatch: Function, // eslint-disable-line flowtype/no-weak-types
     98:   pathname: string
     99: } & OwnProps
         ^ object type
    Member 2:
     99: } & OwnProps
             ^^^^^^^^ OwnProps
    Error:
     48:     isActive?: (?Object, Object) => boolean
                         ^^^^^^^ null. This type is incompatible with the expected param type of
     93:   isActive?: (Object, Object) => boolean
                       ^^^^^^ object type
  Member 2:
   40:     <Def, St>(component: Class<React$Component<Def, P, St>>): ConnectedComponentClass<OP, P, Def, St>;
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ polymorphic type: function type. See lib: flow-typed/npm/react-redux_v5.x.x.js:40
  Error:
  109: export default connector(NavLink)
                                ^^^^^^^ property `$call`. Callable signature not found in
   40:     <Def, St>(component: Class<React$Component<Def, P, St>>): ConnectedComponentClass<OP, P, Def, St>;
                                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics of React$Component. See lib: flow-typed/npm/react-redux_v5.x.x.js:40

As a workaround, I added the following lines to my .flowconfig:

[ignore]
<PROJECT_ROOT>/node_modules/redux-first-router-link/dist/NavLink.js.flow

document importance of middleware order in compose call

I just spent a lovely bit of time trying to figure out why actions I dispatched from onAfterChange did not do anything. It turns out that the order of your middlewares in the compose call is very important. Mine setup code like this:

    const middlewares = [sagaMiddleware, beaconMiddleware, routeMiddleware] 
    const middlewareEnhancer = applyMiddleware(...middlewares)
    const storeEnhancers = [middlewareEnhancer, routeEnhancer]
    const composedEnhancer = composeWithDevTools(...storeEnhancers)

And the action I dispatched is supposed to be handled by the beacon middleware. What happened is that the dispatch function passed to onAfterChange is not the store dispatch, but a router-specific one that inserts the action at the point of the router middleware in the processing pipeline. In my case that meant no middleware would ever see my action since the router middleware was already the last one.

I am not entirely sure why the dispatch doesn't happen at the root level - I am guessing there is a good reason? It think it would be useful to document the importance of the middleware ordering to save others the time I spent figuring this out.

translatable paths

Thank you for such an awesome routing library, I am going to switch from React Router 4 :)
All in all, I think I know how to cover all cases, except for one - having translatable routes in routesMap:
so, instead of this:

const routesMap = { 
  HOME: '/home'
};

something like this:

const routesMap = { 
  HOME:  : {
    en: '/home', 
    de: '/haus'
  }
};

en or de could be part of redux state.language for example.
Would it be possible to achieve something like that with this library?

In the store, location is sometimes mutated rather than replaced

demo on CodeSandbox: https://codesandbox.io/s/mz9wzojo9j

Click the 'FP' SEO-friendly link.
Then click the one I added below, "FP with query".

Result: the URL is modified (a querystring is added), an alert popup states the new location object is equal (===) to the previous one. However it now contains a query object.

Expected: redux states should never be mutated but replaced, so this equality check should return false.

This will cause issues, for instance, for people relying on PureComponent and similar perf strategies.

querystrings in state

The docs say

Intentionally we have chosen to solely support paths since they are best for SEO and keep the API minimal. You are free to use query strings to request data in your thunks.

This is a bit hand-wavy; I think it could be more helpful to explain what the intention is for thunks.

  • Should they be reading the querystring from the window.location object? If so, how is SSR indtended to work with querystrings?
  • Is the querystring stored in an undocumented part of the reducer state? If so, it should be documented.
  • Are thunks supposed to access the querystring in some other way?

Make onBeforeChange honor promises

The onBeforeChange function is not promise aware. This makes it very hard to involve any async operation to decide whether we cancel the route (through dispatching a redirect) or not.

Would be great to have a onBeforeChange function which will honor:

  1. a promise dispatched from within onBeforeChange (like redux-thunk)
  2. or will await a promise returned from onBeforeChange

And will evaluate the "skipping" after the promise resolved.

This will enable a lot of possible use-cases like authentication (an existing cookie/jwt does not guarantee that it is still valid), near real-time stock/reservation checks, lock/conflict handling etc.

Looking into the code it seems feasible without a lot of change.

If this is something you would consider as a PR I would start working on it.

Calling the location reducer "location"

Hello,

By using redux-first-router which is amazing and clearly is the tool I always wanted for dealing with routes in redux, I found that calling the location reducer location can lead to dumb avoidable errors.

Indeed when destructing the state in a function, we can mistakenly make calls to window.location xD
Example:

const f = ({location: type}) => console.log(location);

This code won't raise errors, and will log the window.location variable.
If the reducer where called for example routing:

const f = ({routing: type}) => console.log(routing);

It will correctly raise an error.

I'd like to highlight this behavior because it cost me some weird interpretation ^^"
Maybe we should make a line in the README about that or changing the reducer name in the examples.
I know it is something small and not an issue, but wanted to share it for others ;)

Thanks,

dispatching redirect on client fires two actions

Given this code in my options:

export default {
    onBeforeChange: (dispatch, getState, action) => {
        if( action.type !== 'LOGIN' ) {
            const loginAction = {type: 'LOGIN'};
            const _loginRedirect = redirect( loginAction );
            dispatch( _loginRedirect );
        }
    }
}

With these routes:

export default {
    HOME: '/',
    FORGOT_PASSWORD: '/forgot-password',
}

Why does dispatching {type: 'FORGOT_PASSWORD'} result in two HOME actions being fired? Both appear identical and happen near instantaneously:

doublehome

Is this something I am causing to happen? Is it intentional? If so, what is the rationale behind it. Just curious if there's some benefit to it being called twice, or if it's a bug.

`onAfterChange` and redirects

Before diving into a PR, I'd like to discuss this to determine whether the behaviour is intended.

Currently, onAfterChange runs even when the route redirects. This can result in onAfterChange running twice with the same result, as the middleware will have already changed the current page (via getState()).

Some of this could be due to a race condition in my particular case (async functions), but it seems to be that the change should "resolve" prior to running a function like onAfterChange.

Agree or disagree?

`redirect(...) => action.meta.location.current === undefined`

(action.meta.location.current.pathname !== state.pathname ||

action.meta.location = action.meta.location || {}

Using the redirect ActionCreator results an undefined current location object, and throws when it hits the reducer.

I'd like to nail this down for you and create a PR, but I don't think I'll have time today. Thanks for your work on this so far. ๐Ÿ˜„

pathToRegexp.compile import issues in node 8

Hey,

really sweet project! Tons of kudos. One issue: I'm unable to match actions to routes using node 8.2.1.

The issue seems to stem from the way pathToRegexp.compile is imported in node 8. My project currently requires path-to-regexp at the root node_modules level of the project, and somehow this messes up the import in actionToPath.js. The result is a silent error when identifying path in actionToPath.js which in turn makes all actions result in route not found.

I can resolve the issue by require the import in the following way:

const {compile} = require('../../node_modules/path-to-regexp');

and then use compile as is to evaluate the path.

I haven't issued a PR as the test suite seems to be failing in both node 6 and 8. I don't know if this way of require will break on other versions of node or further complicate stuff, so let me know if you want any more info/help to fix it.

Server-Rendering docs - where is `thunk`?

@faceyspacey first I want to say fantastic work on all of your libs for react + ssr + code splitting - doing myself a deep-deep dive the last several days to get something correctly started, and I came across all of your stuff today and it's been a godsend.

I have a question and a comment about the server-rendering docs.

Question
In configureStore.js you make the inline comment

// notice `thunk`

but don't do anything with thunk in that file. (this would normally fail eslinting)

In serverRender.js you have the line

await thunk(store) // THE WORK: if there is a thunk for current route, it will be awaited here

but I don't see thunk imported or declared anywhere.

Can you please provide more explicit clarity on what to do with thunk returned from connectRoutes(โ€ฆ) as well as how to retreive it for calling in serverRender.js?

Comment
In serverRender.js, it is not idiomatic to send redirects which should result in HTTP 404 (aka NOT_FOUND). Redirecting may be what's needed client-side as there's no way to provide a 404 in an SPA, but according to the HTTP spec and what Google bot expects, the server should always return a 404 when a path does not match a resource on the server.

Initial load action not triggering thunk (client-only use case)

Hello! First off: awesome library, I really like the idea behind it!

I'm currently working on an app which will not have SSR.

I'm just refactoring the data fetching strategy from componentDidMount to within thunks. (As outlined here: https://medium.com/faceyspacey/redux-first-router-data-fetching-solving-the-80-use-case-for-async-middleware-14529606c262)

However I've noticed that on the initial load action. (I.e. I go to /, triggering the respective action defined in routesMap) it doesn't trigger the thunk. Now if I understand correctly this has to do with the SSR-aligned philosophy: The state should come from initial state from the reducer when rendering on the server. In my case however this is not viable because I don't have a server behind the app which can create initial state.

My question is this: What's the recommended way of doing this? What am I missing?

Broken links in FAQ

The links under the question Does this work with React Native? are not valid. I think they probably have to point to .../docs/react-native.md instead of .../docs/react-navigation but I'm not sure (which is why I didn't create any pull request).

ps: and thanks for being elaborate in your FAQ answers. It's really helpful for us less experienced developers ๐Ÿ‘ . Looking forward to your big end-of-june update to see if this package might help me overcome some of react-navigation's routing difficulties I'm experiencing.

TypeScript support?

Just finished ripping out react-router and using this in its place...so far so (very very) good.

One pain point - any chance you can magic up a typings file for the TypeScript users of the world?

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.