faceyspacey / redux-first-router

🎖 seamless redux-first routing -- just dispatch actions
MIT License
1.56k stars 143 forks source link

Update routesMap dynamically #62

Closed qw-in closed 7 years ago

qw-in commented 7 years ago

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.

faceyspacey commented 7 years ago

Comin in a week or so. U gotta make the middleware respond to an action containing the new routes. As well as the reducer which also maintains a reference to it (but that is more standard).

It also may be possible to just update the reducer state and always read from state rather than closure. In a few places that might not be possible, but hopefully that's not the case.

GuillaumeCisco commented 7 years ago

Dynamically injecting routes would be great! I was looking for this feature too but did not find it.

Would be great being able to inject new routes when we async load a component we want to display. It's really a pain to have to declare all the possible routes at first.

Thanks for explanation btw

qw-in commented 7 years ago

Assuming routesMap can always be read from state, is there an argument to be made for not adding a specific action (like NOT_FOUND) for modifying it?

// ...
const { reducer } = connectRoutes(history, routesMap);

// Note the lack of default value
const myReducer = (lastState, action) => {
  const state = reducer(lastState, action);

  switch (action.type) {
    case 'MY_ACTION': {
      return {
        ...state,
       routesMap: {
          ...state.routesMap,
         SOME_NEW_PATH: '/some/new/:path',
       },
      };
    }
    default:
      return state;
  }
};

const rootReducer = combineReducers({ location: myReducer });
// ...

Would be interested in your thoughts or any issues with this approach

faceyspacey commented 7 years ago

a specific action is exactly what it will need no matter what, but i still need to look into the middleware and see if it can read from state.

faceyspacey commented 7 years ago

ok, this is done. it's in the @next dist-tag on NPM. get it like this:

yarn upgrade redux-first-router@next

here's how you use it:

import { addRoutes } from 'redux-first-router'
import universal from 'react-universal-component'

const UniversalComponent = universal(props => import(`./${props.page}`), {
   onLoad: module => {
       const aThunk = addRoutes(module.newRoutes)
       store.dispatch(aThunk)
   } 
})

this is just an example using Universal. you can use any code splitting component/tools you want.

So essentially you have an action creator now to dispatch new routes received via code-splitting.

There's one thing I'm noticing now that I'm using it with React Universal Component--onLoad doesn't receive props, so a global store variable had to be imported and used, which is no good. I'll have to add a 2nd props arg there so your redux connected component can pass dispatch. It should be:

onLoad: => (module, props) => props.dispatch(addRoutes(module.newRoutes))

I'll make that tweak in RUC.

For now if you're looking at this and interested, find a way to access store directly or if you're using other code splitting tools, you know what to do.

Otherwise, i'm closing this issue. feel free to open any specific new Issues. Feel free to add some docs about this. 🚀

qw-in commented 7 years ago

Thanks! Looks awesome!

Unfortunately don't have time to mess with it at the moment but looking forward to it. If there is still a need for docs when I get time to check it out I'll put something together. Maybe this weekend

faceyspacey commented 7 years ago

Ok, React Universal Component has been updated to better support the various things you might wanna do when code-splitting. The onLoad option now receives both props and context. Context is passed so you don't need to import your store, which is problematic when doing SSR where you must have the store corresponding to the specific express request. Here's what this looks like:

import { addRoutes } from 'redux-first-router'
import universal from 'react-universal-component'

const UniversalComponent = universal(props => import(`./${props.page}`), {
   onLoad: (module, info, props, context) => {
     const aThunk = addRoutes(module.newRoutes) // export new routes from component file
     context.store.dispatch(aThunk)

     context.store.replaceReducer({ ...otherReducers, foo: module.fooReducer })

     // if a route triggered component change, new reducers needs to reflect it
     context.store.dispatch({ type: 'INIT_ACTION_FOR_ROUTE', payload: { param: props.param } })
  }
})

do yarn upgrade react-universal-component if that's what you're using for code splitting.

brianephraim commented 7 years ago

addRoutes is missing from npm versions later than 0.0.9-next

faceyspacey commented 7 years ago

@defualt sorry about that. it's back in.

+ redux-first-router@0.0.20-next

rcalabro commented 7 years ago

@faceyspacey can you share your configureStore from this last example? `import { addRoutes } from 'redux-first-router' import universal from 'react-universal-component'

const UniversalComponent = universal(props => import(./${props.page}), { onLoad: (module, info, props, context) => { const aThunk = addRoutes(module.newRoutes) // export new routes from component file context.store.dispatch(aThunk)

 context.store.replaceReducer({ ...otherReducers, foo: module.fooReducer })

 // if a route triggered component change, new reducers needs to reflect it
 context.store.dispatch({ type: 'INIT_ACTION_FOR_ROUTE', payload: { param: props.param } })

} })`

How are you combining the new reducer with the already loaded ones and keeping the state?