Reselect createSelector by id
Asked Answered
K

2

6

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:

  • Is it right approach to get single post?
  • If it is, how to pass ID of the post if not, how to handle this the right way?
Kemberlykemble answered 26/5, 2021 at 5:40 Comment(0)
C
3

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.

Cardcarrying answered 26/5, 2021 at 7:53 Comment(0)
M
1

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);
Monochromatism answered 26/5, 2021 at 6:2 Comment(2)
What about performance issues because of postDetailMap call on each render?Flue
@АртёмГанев Reselect creates memoized selectors, so this postDetailMap selector only recomputes when any of its input selectors update. The performance issue should be minimal, if any at all.Monochromatism

© 2022 - 2025 — McMap. All rights reserved.