react-redux-selector-utils

ultimate redux selector utils

Usage no npm install needed!

<script type="module">
  import reactReduxSelectorUtils from 'https://cdn.skypack.dev/react-redux-selector-utils';
</script>

README

react-redux-selector-utils

this is a small react redux selector utils package that will help you to define and use your selectors in a clean, easy and fast way.

more specifically it will help you to

  • define all your selectors in a linear way
  • one and only one way to combine selecotrs
  • use named selector hooks to avoid importing a lot of selectors in a file and have a fast access to all your selectors.

this is more a library for lazy developers like me! :-)

Install

npm install --save react-redux-selector-utils

Package content

Here are what you can import from the package

  • mapSelectors,
  • createNamedSelectorHook,
  • createSelectorHook,
  • useSelector
import {
  mapSelectors,
  createNamedSelectorHook,
  createSelectorHook,
  useSelector
  } from 'react-redux-selector-utils'
}

mapSelectors

type MapSelectors = <
  Selectors extends Record<any, Selector>,
  MappedSelectedValues = { [K in keyof Selectors]: ReturnType<Selectors[K]> }
>(
  selectors: Selectors,
  selectValue: (map: MappedSelectedValues, state: State) => SelectedValue
) => NewSelector

this helper uses createSelector from reselect and brings out 2 main benefits

  • a simple way to combine selectors using records
const getMin = (state: State) => state.min
const getMax = (state: State) => state.max
// now if we want to get the difference between the max and the min,
// taking account of the state offset, we can proceed like this
const getOffsetDiff = mapSelectors(
  // selectors are passed using a record,
  // so the mapping is done directly here
  { min: getMin, max: getMax },
  (
    map, // map signature is { min: number, max: number }, => low risk to have typo when accessing selected value
    state: State
  ) => state.offset + map.max - map.min
)
// try this simple example using createSelector to see the difference
  • no extra efforts needed when combining selectors with params: the synthax does not change
const getUserById = (state: State, id: string) => state.users[id]
const getPostByIndex = (state: State, index: number) => state.posts[index]
const getUserAndPost = mapSelectors(
  { user: getUserById, post: getPostByIndex },
  (map) => map // map here is { user: User, post: Post }
)
// instead if we used createSelector itself, the synthax would be a curry function
// and a bit complex
const getUserById = (id: string) => (state: State) => state.users[id]
const getPostByIndex = (index: number) => (state: State) => state.posts[index]
const getUserAndPost = (id: string, index: number) =>
  // curry function
  createSelector(
    // selectors are passed in an array and should not be curry functions
    [
      // so we need to pass all the parameters again here
      getUserById(id),
      getPostByIndex(index)
    ],
    // the mapping is done in the callback params
    // => high risk of introducing bad mapping while dealing with a lot of selectors:
    // => we can easily define (post, user) => any instead of (user, post) => any
    (user, post) => ({
      user,
      post
    })
  )

let's now have a look to this selectors.ts file example to see how it will look like in a real project

import { mapSelectors } from 'react-redux-selector-utils'
type State = {
  group: string
  creationDate: string
  users: { [K: string]: User }
  posts: { [K: number]: Post }
}
// no params selector
export const getGroup = (state: State) => state.group
export const getCreationDate = (state: State) => state.creationDate
// selector with params
export const getUserById = (state: State, id: string) => state.users[id]
export const getPostByIndex = (state: State, index: number) => state.posts[index]
// combining no params selectors
export const getGroupAndCreationDate = mapSelectors(
  // selectors record
  { group: getGroup, date: getCreationDate },
  (map) => map // map signature is { group: string, date: string }
)
// this will return the following selector
// (state: State) => { group: string, date: string }

// mixing selectors where some of them require params: same synthax as before
export const getUserAndGroup = mapSelectors(
  {  user: getUserById, group: getGroup, },
  (map) => map // map signature is  { user: User, group: string }
)
// this will return the following selector
// (state: State, params: { user: string }) => { user: User, group: string }

// here you can notice that, the params is a record and has only one key
// "user" which is what will be passed to getUserById selector.

// combining selectors where all of them require params: again same synthax
export const getUserAndPost = mapSelectors(
  {  user: getUserById, post: getPostByIndex },
  (map) => map // map signature is { user: User, post: post }
)
// this will return the following selector
// (state: State, params: { user: string, post: number })
//            => {  user: User, post: Post }

// here instead, we passed 2 selectors and the params record has also 2 keys
// "user": parameter to be passed to getUserById selector
// "post": parameter to be passed to getPostByIndex selector
}

now we we can use these selectors in our component MyComponent.tsx so we need to import useSelector from react-redux-selector-utils

import { useSelector } from 'react-redux-selector-utils'
import {
  getGroup,
  getUserById,
  getCreationDate,
  getGroupAndCreationDate,
  getUserAndGroup,
  getUserAndPost
  } from './selectors'

const MyComponent = () => {
  // how to use a no params selector
  const group = useSelector(getGroup)
  const date = useSelector(getCreationDate)
  // how to use a no params combined selector
  const groupAndDate = useSelector(getGroupAndCreationDate)
  // how to use a selector with params
  const user = useSelector(getUserById, "userId")
  const userAndGroup = useSelector(getUserAndGroup, { user: "userId" })
  const userAndPost = useSelector(getUserAndPost, {
    user: 'userId',
    post: 3
  })
  return <div>test</div>

useSelector

as we can see, to use selecotrs in our component, like in react-redux, we need a hook. In our case, we need the useSelector hook from our package that is a wrapper that takes 1 or 2 arguments:

  • a given selector: first and required argument
  • the selector's parameter: is required only when the given selector requires a parameter

we'll talk about it in createSelectorHook section.

createNamedSelectorHook

in the previous example we saw how to use our selectors but one thing we can notice is that we had to import all the desired selectors in our component file. And this is an operation we will often do. That means, we should deal many times with the following situations:

  • repetitive imports: we are importing the same selector functions many times in different files => more line of codes => more file size

  • conflicting selectors: let's imagine we have both getX and getY from ./pippo/selectors.ts and ./pluto/selectors.ts. it can happen to import the wrong getX or the wrong getY due to the multiple imports we can have in our file. But more often, we'll have to import both of them in the same file, and in this case, to avoid conflictual imports, we need to rename all those selectors like follows:

import { getX as getPippoX, getY as getPippoY } from '/pippo/selectors'
import { getX as getPlutoX, getY as getPlutoY } from '/pluto/selectors'

these situations are not a big deal, but since they will come over and over in our daily work, using a named selector hook will eventually have a global impact at the end of the day because it will help us to have a clean file with less imports, to avoid selectors conflict and have a fast access to all our selectors.

so let's then define a hooks.ts file where we can create our a named selector hook and see how to use it

import { createNamedSelectorHook } from 'react-redux-selector-utils'
import * as selectors from './selectors'

export const useUserSelectors = createNamedSelectorHook({
  selectors
  // withShallowEqual?: boolean // to use a shallowEqual selector hook
  // context?: YourReduxContext
})
// a variant
export const useUserSelectorsShallEq = createNamedSelectorHook({
  selectors,
  withShallowEqual: true
  // context?: YourReduxContext
})

now we defined the useUserSelectors, we can change the MyComponent.tsx file removing all the imported selectors, import only our named hook and use it instead of useSelector

// we have only 1 import here instead of 7
import { useUserSelectors } from './hooks'
// import { useSelector } from 'react-redux-selector-utils'
/*import {
  getGroup,
  getUserById,
  getCreationDate,
  getGroupAndCreationDate,
  getUserAndGroup,
  getUserAndPost
  } from './selectors'
  */

const MyComponent = () => {
  // how to use a no params selector
  const group = useUserSelectors('getGroup')
  const date = useUserSelectors('getCreationDate')
  // how to use a no params combined selector
  const groupAndDate = useUserSelectors('getGroupAndCreationDate')
  // how to use a selector with params
  const user = useUserSelectors('getUserById', 'userId')
  const userAndGroup = useUserSelectors('getUserAndGroup', { user: 'userId' })
  const userAndPost = useUserSelectors('getUserAndPost', {
    user: 'userId',
    post: 3
  })
  return <div>test</div>
}

Also here, the named selector hook accepts 1 or 2 arguments.

  • the selector name: first and required argument, a string reference of a desired selector
  • the selector's parameter: required if the given selector from the selector name requires a parameter

as you can see, in this case, we directly also avoid selectors names conflicts

// import { getX as getPippoX, getY as getPippoY } from '/pippo/selectors'
// import { getX as getPlutoX, getY as getPlutoY } from '/pluto/selectors'
import { usePippoSelectors } from '/pippo/hooks'
import { usePlutoSelectors } from '/pluto/hooks'

const MyComponent = () => {
  const pippoX = usePippoSelectors('getX')
  const plutoX = usePlutoSelectors('getX')
  return <div>test</div>
}

createSelectorHook

this is an helper that takes optionally an object { context?, withShallowEqual? } and returns a selector hook.

the default imported useSelector is defined using createSelectorHook without params

export const useSelector = createSelectorHook()

We can define useSelector variants using the optional parameter (the same way we do with named hooks)

export const useShallowSelector = createSelectorHook({ withShallowEqual: true })
export const useSelector2 = createSelectorHook({ context: context2 })
export const useShallowSelector2 = createSelectorHook({
  context: context2,
  withShallowEqual: true
})

try it out

Github

react-redux-selector-utils

see also