Reselect: Best Practices - Should selectors be 1-to-1 with reducers?

Created on 17 Mar 2016  路  8Comments  路  Source: reduxjs/reselect

I saw a twitter post by Dan Abramov recommending to export selectors with related reducers, and I was wondering, should selectors in general be 1-to-1 with reducers? Or is it perfectly OK to have selectors select state across multiple reducers, all depending on what information a component needs?

I seem to have selectors be doing two different things right now. Some selectors will select only the necessary state out of a specific reducer or specific reducers (so their only job is to pick out substates of my reducers), and then I have some selectors compute data based on information in a specific reducer or specific reducers (like redux's computing derived data section). I also have selectors that do a combination of the two.

One more thing, is it a good practice to ALWAYS access application state in reducers through selectors? (I use redux-saga which provides the "select" function and I think promotes that idea)

Anyway my selectors seem pretty unorganized to me, and I'm not sure what I'm doing wrong. I was hoping to get a sense of how to use selectors in a large application.

Most helpful comment

I'm using one selector per container instead. So I keep my reducers with the _raw_ data while I compose more concrete data with selectors in every container. In fact, I'm using them in the 3rd argument of the redux's connect method: mergeProps

connect(mapStateToProps, actions, mergeProps)(Container)

This way I can build callbacks for the view models combining store data with actions.

For instance:

const store = {
  songs: [
    {id: 1, title: 'Maybe', artist: 'Oasis'},
    {id: 2, title: 'Creep', artist: 'Radiohead'},
    {id: 3, title: 'Desorden', artist: 'Los Planetas'},
  ],
  playlist: [
    {id: 1, title: 'foo', songs: [1, 2]}
    {id: 2, title: 'bar', songs: [3]}
  ],
  currentPlaylist: 1
}

// actions
function playSong(songId) {
  return {
    type: 'PLAY_SONG',
    songId
  }
}
// in container
connect(mapStateToProps, {playSong}, playlistViewSelector)(PlaylistView)

const mapStateToProps = (state) => {
  return {
    playlist: state.playlist,
    songs: state.songs,
    currentPlaylist: currentPlaylist
  }
}

const playlistViewSelector = createSelector(
  (state) => state.playlist,
  (state) => state.songs,
  (state) => state.currentPlaylist,
  (state, actions) => actions.playSong,
  (playlist, songs, currentPlaylist, playSong) => {
    return {
      songs: playlist.find(p => p.id === currentPlaylist)
        .map(songId => songs.find(song => song.id === songId))
        .map(song => return {
           name: song.name,
           play: () => playSong(song.id)
        })
     }
})

The main point is the play method, so Container doesn't compose any data, this is done by the selector.
Good things on doing this way: It's pretty easy to test and removes logic from Containers.

In fact, in this case, we could create another selector to get the songs objects per playlist and use it in this selector, so we would get:

const playlistViewSelector = createSelector(
  songsPerPlaylistSelector
  (state, actions) => actions.playSong,
  (songsPerPlaylist, playSong) => {
    return {
      songs: songsPerPlaylist.map(song => return {
           name: song.name,
           play: () => playSong(song.id)
        })
     }
})

Sorry for the long answer, I hope you find it useful.

All 8 comments

I'm using one selector per container instead. So I keep my reducers with the _raw_ data while I compose more concrete data with selectors in every container. In fact, I'm using them in the 3rd argument of the redux's connect method: mergeProps

connect(mapStateToProps, actions, mergeProps)(Container)

This way I can build callbacks for the view models combining store data with actions.

For instance:

const store = {
  songs: [
    {id: 1, title: 'Maybe', artist: 'Oasis'},
    {id: 2, title: 'Creep', artist: 'Radiohead'},
    {id: 3, title: 'Desorden', artist: 'Los Planetas'},
  ],
  playlist: [
    {id: 1, title: 'foo', songs: [1, 2]}
    {id: 2, title: 'bar', songs: [3]}
  ],
  currentPlaylist: 1
}

// actions
function playSong(songId) {
  return {
    type: 'PLAY_SONG',
    songId
  }
}
// in container
connect(mapStateToProps, {playSong}, playlistViewSelector)(PlaylistView)

const mapStateToProps = (state) => {
  return {
    playlist: state.playlist,
    songs: state.songs,
    currentPlaylist: currentPlaylist
  }
}

const playlistViewSelector = createSelector(
  (state) => state.playlist,
  (state) => state.songs,
  (state) => state.currentPlaylist,
  (state, actions) => actions.playSong,
  (playlist, songs, currentPlaylist, playSong) => {
    return {
      songs: playlist.find(p => p.id === currentPlaylist)
        .map(songId => songs.find(song => song.id === songId))
        .map(song => return {
           name: song.name,
           play: () => playSong(song.id)
        })
     }
})

The main point is the play method, so Container doesn't compose any data, this is done by the selector.
Good things on doing this way: It's pretty easy to test and removes logic from Containers.

In fact, in this case, we could create another selector to get the songs objects per playlist and use it in this selector, so we would get:

const playlistViewSelector = createSelector(
  songsPerPlaylistSelector
  (state, actions) => actions.playSong,
  (songsPerPlaylist, playSong) => {
    return {
      songs: songsPerPlaylist.map(song => return {
           name: song.name,
           play: () => playSong(song.id)
        })
     }
})

Sorry for the long answer, I hope you find it useful.

No hey the longer the better just trying to learn here. Just a few questions:

  1. How large is your application?
  2. Doesn't coupling selectors with containers take away the reusability of selectors?
  3. What if you want to use selectors with redux-saga or redux-thunk?

I ask that last question because I've seen some advocate for using selectors everywhere in code so that selectors are the only thing that need to know the shape of the application state, and in the event that your state shape changes, you only need to worry about the selectors instead of also going into your thunks or sagas and changing how data is accessed (which I think is why Dan A. had mentioned exporting selectors with related reducers).

  1. We're building a backoffice so it's growing fast.
  2. Not really because many times containers need more processed data that is build by the combination of different selectors, then you start reusing them.
  3. Don't get this. My sagas just dispatch more actions according to some events, but the data only changes on the reducers and is mapped for containers on my selectors.

This implementation works form me as keep selectors easy to group, find and read; but don't take this as the only way to doing it.

Closing due to inactivity

my bad, I was hoping to get some other responses, but it doesnt look like anyone else has much to say anyway!

Hey @nickzarate, I didn't mean to imply you had done anything wrong! I was doing some tidying up and it just looked like the conversation had dried up.

Had the very same question. My thoughts are to lean toward always creating selectors. I found that practice helpful when moving part of your state to a new section of the tree. Application features continue to evolve over time and it's inevitable that your state tree will change with it.

We currently keep selectors outside of reducers as well. This helps when we combine selectors that grab different parts of the state tree.

I like the idea of storing selectors within reducer files, keeping selectors within the context of reducers makes sense. I could see importing selectors from other reducers files to work just fine. my only question would be which reducer imports the selector. Might make sense to have a vm specific reducer that imports selectors from a reducer that reflects the state on the server.

For future reference, this is the tweet from Dan Abramov: https://twitter.com/dan_abramov/status/664581975764766721

Was this page helpful?
0 / 5 - 0 ratings