connected-react-router icon indicating copy to clipboard operation
connected-react-router copied to clipboard

Access match object in reducer

Open joaosoares opened this issue 7 years ago • 21 comments

It doesn't seem like it is possible to access the match object in the reducer. Should there be a way to connect a specific Route so that its state is saved to the reducer as well? Something like

import { ConnectedRouter, ConnectedRoute } from 'connected-react-router'
...
<ConnectedRouter history={history}>
  <ConnectedRoute path='/example/:willshow' />
  <Route path='/example/:wontshow' />
</ConnectedRouter>

that would generate the state

{
  history: {
    location: {
      path: '/example/somevalue'
    }
  },
  match: {
    params: {
      willshow: 'somevalue'
    },
    // ... React-Router match object (from https://reacttraining.com/react-router/#match)
  }
} 

This would basically make the reducer state be the same as the Router object received from the withRouter connector.

joaosoares avatar Feb 21 '17 02:02 joaosoares

IMO, the reducer state cannot be the same with the router object received from withRouter.

With withRouter, it provides props for a single route. However, the proposed ConnectedRoute can be applied to multiple routes.

We might provide additional key prop to ConnectedRoute like this:

import { ConnectedRouter, ConnectedRoute } from 'connected-react-router'
...
<ConnectedRouter history={history}>
  <ConnectedRoute key="parent" path='/example/:username' render={({match}) => (
    <div>
      {match.params.username}
      <ConnectedRoute key="child" path={`${match.url}/:item`} render={({match}) => (
        {match.params.item} 
      )} />
    </div>
  )} />
</ConnectedRouter>

Then, if the URL is /example/supasate/pencil, the reducer state might be like this:

{
  history: {
    location: {
      path: '/example/somevalue'
    }
  },
  matches: {
    parent: {
      params: {
        username: 'supasate',
      },
    },
    child: {
      params: {
        item: 'pencil',
      },
    }
  }
} 

However, the mapStateToProps needs to know the key name which makes it tight coupling with the route. So, I don't think it's the right way to go.

If you have any idea, feel free to discuss :)

supasate avatar Feb 21 '17 09:02 supasate

That's true. Maybe we could connect the <Switch /> components, since they match exclusively with a route?

The key property would still have to be set, but the coupling would happen on a higher-level, as there are usually fewer switches than routes and they're less likely to change dynamically.

<ConnectedRouter history={history}>
  <ConnectedSwitch key="mainnav">
    <Route path='/example/:username' render={({match}) => (
      <div>
        {match.params.username}
        <Route path={`${match.url}/:item`} render={({match}) => (
          {match.params.item} 
        )} />
      </div>
    )} />
    <Route path ='/other/:id' component={OtherComponent}/>
  </ConnectedSwitch>
</ConnectedRouter>

The state for /example/supasate could be

{
  /* ... */
  matches: {
    mainnav: {
      params: {
        username: 'supasate',
      },
    }
  }
} 

And the state for /other/someid would be

{
  /* ... */
  matches: {
    mainnav: {
      params: {
        id: 'someid',
      },
    }
  }
} 

As for the child route, it'd only be connected if it's enclosed on its own <Switch />

joaosoares avatar Feb 21 '17 16:02 joaosoares

Yes, the coupling still exists. A component needs to know that it will be wrapped in which key of ConnectedSwitch.

Compared to connecting a route directly with ConnectedRoute in the previous comment, I prefer ConnectedRoute to ConnectedSwitch because it's quite straightforward about mapping one-to-one of the match we need and the state in redux store. Anyway, I still don't like how coupling it is.

We may keep this issue open and see other ideas if there is the same demand on this feature.

supasate avatar Feb 22 '17 02:02 supasate

I agree this still needs some thought. For now, the workaround I found was to create a static route config and use the matchPath function provided by React Router to avoid parsing the current path manually.

joaosoares avatar Feb 22 '17 21:02 joaosoares

@joaosoares could you also share the way you are doing with matchPath? I am using createMatchSelector in react-router-redux and wondering if there is a same method in connected-react-router.

trungdq88 avatar Jun 07 '18 00:06 trungdq88

Try this:

import { matchPath } from 'react-router-dom';

const match = matchPath(
    router.location.pathname, // like: /course/123
    { path: '/course/:id' }
  );
const id = match.params.id; // get: 123

brneto avatar Jun 07 '18 04:06 brneto

Any headway on this issue? It seems like a major limitation to not have access to the match properties in redux (for use in thunks)

winterspan avatar Jun 29 '18 23:06 winterspan

I'm also suprised this isn't a basic feature. For now I'm using matchPath.

jorgenbs avatar Sep 03 '18 08:09 jorgenbs

Whats the point of having the key prop? Can't we just merge the state?

{
  history: {
    location: {
      path: '/example/somevalue'
    }
  },
  matches: {
    params: {
      username: 'supasate',
      item: 'pencil'
    }
  }
}

piu130 avatar Sep 13 '18 20:09 piu130

@jorgenbs @trungdq88 you can do this way too:

import { createMatchSelector } from 'connected-react-router';

const matchSelector = createMatchSelector({ path: '/course/:id })
const match = matchSelector(state) // like: /course/123
const id = match.params.id; // get: 123

brneto avatar Oct 05 '18 00:10 brneto

@brneto awesome thanks !

seb-bizeul avatar Oct 10 '18 08:10 seb-bizeul

+1 lets get match params into state

json2d avatar Dec 15 '18 14:12 json2d

+1 for match params into state

JacksonToomey avatar Jan 18 '19 00:01 JacksonToomey

My thumb up to @brneto, but can't do import { createMatchSelector } from 'connected-react-router'; in TypeScript envirement so I used import {createMatchSelector} from 'connected-react-router/esm/'; Maybe wrong but works

strobox avatar Jan 20 '19 10:01 strobox

Anyway, @brneto , I also +1 for match params into state, because otherwise it is impossible to create reusable component with nested routes:


const Topics = () => {
  const match = useStore(state => ❓matchSelector❓(state)); 
  /* 🤔 how can I define 
       const matchSelector = createMatchSelector({ path: '....' })
      if I even didn't know at which path my re**usable** component will appear
  */
  return (
  <div>
    <h2>Topics</h2>
        <Link to={`${match.url}/details`}>Show Details</Link>
    </ul>
    <Route path={`${match.path}/:id`} component={TopicDetails} />
  </div>);
}

strobox avatar Jan 20 '19 11:01 strobox

It is very elegant and convenient to use:

import useReactRouter from 'use-react-router';
const { history, location, match } = useReactRouter();

for those who already use react hooks, but it will be better to have same ability in connected-react-router

strobox avatar Jan 20 '19 11:01 strobox

@jorgenbs @trungdq88 you can do this way too:

import { createMatchSelector } from 'connected-react-router';

const matchSelector = createMatchSelector({ path: '/course/:id })
const match = matchSelector(state) // like: /course/123
const id = match.params.id; // get: 123

The problem of this method, you must know exact path matcher. If you have two paths in one epic that loads box, like:

  • /boxes/<box_id>/
  • /boxes/edit/<box_id> You have to check both.

firov avatar Jan 23 '19 11:01 firov

@joaosoares did your technique evolved? BTW , I guess the following apply to connected-react-router too.

Source: https://github.com/reactjs/react-router-redux#how-do-i-access-router-state-in-a-container-component

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.

My two cents would be... we want Route Paired Data Fetching, as in: internal/external url change -> saga/thunk -> fetch data -> notify reducers

So this boils down to... if we can't get the params from Redux:

  • Cent 1: In Index.js render App including all routes and the exact keyword. This way App will receive location and match properties inside props already parsed
<Provider store={store}>
    <ConnectedRouter history={history} >
        <Route path={["/","/employee/:id", "/all","/employee/:id/all"]} exact component={App}/>
    </ConnectedRouter>
</Provider>

Then in App.js you alert thunk or sagas sending them a LOCATION_SYNC action with location and match like:

dispatch({type: 'LOCATION_SYNC', location: props.location, match: props.match})

thunk/sagas will compare the route and params, verify that the correct employee has already been fetched, and if not go fetch it.

Two lines of code -> go to redux development tools -> see LOCATION_SYNC there 💃

  • Cent 2: catch the LOCATION_CHANGE action in a saga watcher, then parse it ourselves against all our application routes, then decide which one wins and maybe... dispatch an action associated at that particular route, so other saga watchers can continue from there.

Note that all internal app navigation should be done with dispatch(push(url)). In some cases you may want to... after changing something internally... change the url without navigating.

Other than that, it would be great if connected-react-router guys talk with react first router guys and see if they can merge together or at least interact.

  • Here is some discussion about possibilities with a low usage library https://medium.com/faceyspacey/redux-first-router-data-fetching-solving-the-80-use-case-for-async-middleware-14529606c262

  • here a library to quickly match many routes to pick a winner https://www.npmjs.com/package/route-recognizer

zurcacielos avatar Sep 30 '19 21:09 zurcacielos

I don't know if it helps anyone, but I'm using custom route that dispatches an action when it's being accessed:

import React from "react";
import { Route } from "react-router";
import { ReactReduxContext } from 'react-redux'

export const CustomRoute = ({ component: Component, computedMatch, ...rest }) => (
  <Route {...rest} render={props => {
    // custom logic here, if needed

    return <ReactReduxContext.Consumer>
      {({ store }) => {
        store.dispatch({ type: "ROUTE_CHANGED", url: computedMatch.url, path: computedMatch.path, params: computedMatch.params })
        return <Component {...props} />;
      }}
    </ReactReduxContext.Consumer>;
  }
  } />
);

And I use it later as follows:

<ConnectedRouter history={history}>
  <Switch>
    <CustomRoute exact path='/login' component={LoginPage} />
    // ...
  </Switch>
</ConnectedRouter>

It dispatches an action called ROUTE_CHANGED with url, path and params from match object that i can use in any reducer/saga/component.

kjeske avatar Nov 03 '19 21:11 kjeske

@kjeske that's an elegant solution

for my version

  • import { Route } from "react-router-dom";
  • it needs to be inside a

And then I can set in index.js something that notifies in any route, listing all my routes like

      <ConnectedRouter history={history}>
        <Switch>
        <CustomRoute
          path={[
            "/",
            "/results",
            "/results/:id",
            "/pets/:id",
            "/all",
            "/pets/:id/all",
            "/doctors"
          ]}
          exact
          component={App}
        />
        </Switch>
      </ConnectedRouter>
`
The ConnectedRouter sends its own syncing to redux but in a incomplete fashion :_(

zurcacielos avatar Nov 13 '19 21:11 zurcacielos

do we have some updates or estimates when match or params will be available in reducer or in thunk?

R-iskey avatar Jun 05 '20 08:06 R-iskey