How to sync Redux state and url hash tag params
Asked Answered
S

2

78

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.

Steamheated answered 19/4, 2016 at 15:16 Comment(3)
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)?Gadson
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.Steamheated
e.g. use peterbeshai.com/react-url-query ?Dierolf
G
153

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)
Gadson answered 20/4, 2016 at 16:34 Comment(9)
Thanks @dan for your detailed answer. I will try to apply it and report how it worked.Steamheated
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?Steamheated
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.Gadson
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.Ats
@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.Gadson
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.Aldis
@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.Outside
@DanAbramov Regarding your first comment, may I ask what would you pass in the dispatch ? Why it is needed ? Thanks!Palocz
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).Drachm
M
3

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

Module answered 20/4, 2016 at 10:29 Comment(1)
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.Gadson

© 2022 - 2024 — McMap. All rights reserved.