77

We have a list of lectures and chapters where the user can select and deselect them. The two lists are stored in a redux store. Now we want to keep a representation of selected lecture slugs and chapter slugs in the hash tag of the url and any changes to the url should change the store too (two-way-syncing).

What would be the best solution using react-router or even react-router-redux?

We couldn't really find some good examples where the react router is only used to maintain the hash tag of an url and also only updates one component.

Aurélien Ooms
  • 4,834
  • 3
  • 17
  • 28
JoKer
  • 1,064
  • 1
  • 8
  • 11
  • 4
    Why do you want to keep this state in the store, as opposed to just using React Router as the source of truth for URL parameters, and using the props it injects in your mapStateToProps(state, ownProps)? – Dan Abramov Apr 19 '16 at 15:41
  • 2
    Because the url parameters only contain the slugs of the lectures and the chapters which are selected. In the store I have a list of lectures and chapters with a name, slug and a selected Boolean value. – JoKer Apr 20 '16 at 09:58
  • 1
    e.g. use https://peterbeshai.com/react-url-query/ ? – mb21 Mar 06 '18 at 13:17

2 Answers2

150

I think you don’t need to.
(Sorry for a dismissive answer but it’s the best solution in my experience.)

Store is the source of truth for your data. This is fine.
If you use React Router, let it be the source of truth for your URL state.
You don’t have to keep everything in the store.

For example, considering your use case:

Because the url parameters only contain the slugs of the lectures and the chapters which are selected. In the store I have a list of lectures and chapters with a name, slug and a selected Boolean value.

The problem is you’re duplicating the data. The data in the store (chapter.selected) is duplicated in the React Router state. One solution would be syncing them, but this quickly gets complex. Why not just let React Router be the source of truth for selected chapters?

Your store state would then look like (simplified):

{
  // Might be paginated, kept inside a "book", etc:
  visibleChapterSlugs: ['intro', 'wow', 'ending'],

  // A simple ID dictionary:
  chaptersBySlug: {
    'intro': {
      slug: 'intro',
      title: 'Introduction'
    },
    'wow': {
      slug: 'wow',
      title: 'All the things'
    },
    'ending': {
      slug: 'ending',
      title: 'The End!'
    }
  }
}

That’s it! Don’t store selected there. Instead let React Router handle it. In your route handler, write something like

function ChapterList({ chapters }) {
  return (
    <div>
      {chapters.map(chapter => <Chapter chapter={chapter} key={chapter.slug} />)}
    </div>
  )
}

const mapStateToProps = (state, ownProps) => {
  // Use props injected by React Router:
  const selectedSlugs = ownProps.params.selectedSlugs.split(';')

  // Use both state and this information to generate final props:
  const chapters = state.visibleChapterSlugs.map(slug => {
    return Object.assign({
      isSelected: selectedSlugs.indexOf(slug) > -1,
    }, state.chaptersBySlug[slug])
  })

  return { chapters }
}

export default connect(mapStateToProps)(ChapterList)
Alireza
  • 83,698
  • 19
  • 241
  • 152
Dan Abramov
  • 241,321
  • 75
  • 389
  • 492
  • Thanks @dan for your detailed answer. I will try to apply it and report how it worked. – JoKer Apr 22 '16 at 13:47
  • 4
    sorry for coming back so late but I was on holidays the last week. I have one question. You described one way which is getting the data from the url and rendering it. How would you update the url with the new selected chapters? – JoKer May 02 '16 at 15:55
  • 9
    I would call `browserHistory.push()` (where `browserHistory` is imported from `react-router`) in the action creator. I would probably use Redux Thunk to make it clear there’s a side effect going on. I’d call `dispatch({ ... }); browserHistory.push(...)` inside it. – Dan Abramov May 02 '16 at 20:13
  • 3
    Hey Dan. Thanks for Redux! My I also put my 2 cents in the topic and ask a question. What if an application state-transitioning logic is dependent on the data in a query? That means the reducers need it and you can't just get rid of it in the store. – Alexander Reshytko May 07 '16 at 19:37
  • 2
    @AlexanderReshytko Since this data comes from “external world” I’d emit actions with it *when necessary*. This doesn’t mean you need a generic “route changed” action though (even though you can dispatch it if you find it useful). Your reducers could then use that information, just like they usually do. – Dan Abramov May 08 '16 at 00:42
  • 8
    It's a bit awkward to send parameters to reselect selectors, so being able to get the current url params from the store is very useful. – Jacob Rask May 17 '16 at 07:52
  • @DanAbramov thanks for you answer but I don't get it ! How can you do stuff (like dispatching actions) if the URL data is only available in the Router ? Imagine you build a simple chat, and in the URL hash you put the chatroom name. You load the app with #/some-fun-room in the URL. How can you dispatch action to connect to the room ? I see people dispatch stuff from the components ... it seems completely wrong to me. – lud Jan 06 '17 at 17:18
  • @DanAbramov Regarding your first comment, may I ask what would you pass in the `dispatch` ? Why it is needed ? Thanks! – Ioan Mar 08 '17 at 19:57
  • 2
    Your `mapStateToProps` creates there new object on chapters property of returned props. This will cause the component to rerender every time anything in store changes (because one of props changes on every redux action). – krzychek Jun 07 '17 at 13:32
3

react-router-redux can help you inject the url stuff to store, so every time hash tag changed, store also.

David Guan
  • 3,702
  • 21
  • 29
  • 23
    It just gives you the location, not the parsed params, so it’s of limited usefulness unless you’re fine with reimplementing what React Router gives you manually. It’s a nice solution for keeping store in sync with actions *so that recording and replaying user sessions works* but it’s not really that useful for apps that don’t need this. – Dan Abramov Apr 20 '16 at 20:44