Andrew Losseff
Andrew Losseff

Reputation: 373

Reselect createSelector by id

I am using reselect lib in my React project.

I've created Posts selector which works fine. Here's the code

// selectors.ts

const getPosts = (state: RootState) => state.posts.posts;

export const getPostsSelector = createSelector(getPosts, (posts) => posts);

And I call it on my page like that

// SomePage.tsx    
const posts = useSelector(getPostsSelector);

Now I need to get a post by id. I thought to do it like that:

// selectors.ts

const getPostDetail = (state: RootState) =>
  state.posts.posts.entities[ID];

export const getPostsById = createSelector(
  getPostDetail,
  (detail) => detail
);

And call it on the page:

const PostDetail = useSelector(getPostsById);

I have two questions:

Upvotes: 6

Views: 4100

Answers (2)

DoXicK
DoXicK

Reputation: 4812

The issue here is that react-redux as a HOC has the connect(...) function, which allows selectors to be (state, props) => ..., whereas useSelector only passes (state) => ....

To allow still passing props, you could implement useSelectorWithProps:

function useSelectorWithProps (selector, props, deps, equalityFn) {
  const selectorFn = useCallback(state => selector(state, props), deps)
  return useReduxSelector(
    selectorFn,
    equalityFn
  )
}

This then allows you to do:

// selectors.js

const selectPosts = (state) => state.posts.posts;

const selectPostById = createSelector(
  selectPosts,
  (_, props) => props.id,
  (posts, id) => posts[id]
)

// Or make a utility function for selecting the prop
const selectProp = (key, orDefault) => (_, props) => (props[key] || orDefault)

const selectPostById = createSelector(
  selectPosts,
  selectProp('id'),
  (posts, id) => posts[id]
)

// but don't do this, as it will de-optimize the memoization of reselect.
// `{id: 1} !== {id: 1}` and thus causes it to recalculate the selector
// (Keyword: referential equality)
const selectPostById = createSelector(
  selectPosts,
  (_, props) => props,
  (posts, { id }) => posts[id]
)
// component.js
function Component(props) {
  const value = useSelectorWithProps(
    selectPostById,
    { id: props.id }, // pass an object
    [ props.id ] // pass dependencies of the props-object
  )
}

It's how i've implemented it in the projects i work in.

This allows your full "selector logic" to still live in your selectors, making it easier to test and having to write less glue-code in hooks.

It also allows you to use the same selectors for both connect(...) and useSelectorWithProps(..) might you want to.

Upvotes: 3

Drew Reese
Drew Reese

Reputation: 203512

It is not really the correct approach to select by specific id. The useSelector hook doesn't allow for passing more than just state to the selector.

A roundabout solution could be to store a specific post id also into state and select that as well. The downside is that the post will be unavailable on the initial render and you will need to dispatch (from useEffect hook or callback) an action to store the post id you want.

const getPostById = createSelector(
  [getPostsSelector, getPostId],
  (posts, id) => posts.find(post => post.id === id);
);

Usage:

const postById = useSelector(getPostById);

...

dispatch(setPostId(postId)); // store the id

Since you can't create a selector to return a specific post by id directly, then I suggest to instead create a selector that returns a derived state object of posts that lends itself to faster lookups, i.e. a map or object.

Example:

const postDetailMap = createSelector(
  [getPosts],
  posts => posts.reduce((posts, post) => ({
    ...posts,
    [post.id]: post,
  }), {}),
);

Usage:

const postsMap = useSelector(postDetailMap);

const specificPost = postMap[postId];

You may be able to abstract this into a custom hook though.

const useGetPostById = id => {
  const postsMap = useSelector(postDetailMap);
  return postsMap[id];
}

Usage:

const post = useGetPostById(postId);

Upvotes: 1

Related Questions