Reputation: 373
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
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
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