Skip to content

Commit 6689b10

Browse files
committed
Add selectors section
1 parent 72ed3d4 commit 6689b10

File tree

2 files changed

+183
-16
lines changed

2 files changed

+183
-16
lines changed

docs/tutorials/essentials/part-4-using-data.md

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1052,7 +1052,7 @@ We should now see both sides of the auth behavior working:
10521052
- If the user tries to access `/posts` without having logged in, the `<ProtectedRoute>` component will redirect back to `/` and show the `<LoginPage>`
10531053
- When the user logs in, we dispatch `userLoggedIn()` to update the Redux state, and then force a navigation to `/posts`, and this time `<ProtectedRoute>` will display the posts page.
10541054
1055-
### Showing the Logged-In User
1055+
### Updating the UI with the Current User
10561056
10571057
Since we now know who is logged in while using the app, we can show the user's actual name in the navbar. We should also give them a way to log out as well, by adding a "Log Out" button.
10581058
@@ -1136,6 +1136,52 @@ export const AddPostForm = () => {
11361136
}
11371137
```
11381138
1139+
Finally, it also doesn't make sense to allow the current user to edit posts defined by _other_ users. We can update the `<SinglePostPage>` to only show an "Edit Post" button if the post author ID matches the current user ID:
1140+
1141+
```tsx title="features/posts/SinglePostPage.tsx"
1142+
export const SinglePostPage = () => {
1143+
const { postId } = useParams()
1144+
1145+
const post = useAppSelector(state =>
1146+
state.posts.find(post => post.id === postId)
1147+
)
1148+
// highlight-next-line
1149+
const user = useAppSelector(state => state.auth.username)
1150+
1151+
if (!post) {
1152+
return (
1153+
<section>
1154+
<h2>Post not found!</h2>
1155+
</section>
1156+
)
1157+
}
1158+
1159+
// highlight-next-line
1160+
const canEdit = user === post.user
1161+
1162+
return (
1163+
<section>
1164+
<article className="post">
1165+
<h2>{post.title}</h2>
1166+
<div>
1167+
<PostAuthor userId={post.user} />
1168+
<TimeAgo timestamp={post.date} />
1169+
</div>
1170+
<p className="post-content">{post.content}</p>
1171+
<ReactionButtons post={post} />
1172+
// highlight-start
1173+
{canEdit && (
1174+
<Link to={`/editPost/${post.id}`} className="button">
1175+
Edit Post
1176+
</Link>
1177+
)}
1178+
// highlight-end
1179+
</article>
1180+
</section>
1181+
)
1182+
}
1183+
```
1184+
11391185
### Clearing Other State on Logout
11401186
11411187
There's one more piece of the auth handling that we need to look at. Right now, if we log in as user A, create a new post, log out, and then log back in as user B, we'll see both the initial example posts and the new post.

docs/tutorials/essentials/part-5-async-logic.md

Lines changed: 136 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -217,57 +217,72 @@ So far, our `postsSlice` has used some hardcoded sample data as its initial stat
217217

218218
In order to do that, we're going to have to change the structure of the state in our `postsSlice`, so that we can keep track of the current state of the API request.
219219

220-
### Extracting Posts Selectors
220+
### Extracting Selectors for Slices
221221

222222
Right now, the `postsSlice` state is a single array of `posts`. We need to change that to be an object that has the `posts` array, plus the loading state fields.
223223

224224
Meanwhile, the UI components like `<PostsList>` are trying to read posts from `state.posts` in their `useSelector` hooks, assuming that field is an array. We need to change those locations also to match the new data.
225225

226-
It would be nice if we didn't have to keep rewriting our components every time we made a change to the data format in our reducers. One way to avoid this is to define reusable selector functions in the slice files, and have the components use those selectors to extract the data they need instead of repeating the selector logic in each component. That way, if we do change our state structure again, we only need to update the code in the slice file.
226+
It would be nice if we didn't have to keep rewriting our components every time we made a change to the data format in our reducers. One way to avoid this is to **define reusable selector functions in the slice files**, and have the components use those selectors to extract the data they need instead of repeating the selector logic in each component. That way, if we do change our state structure again, we only need to update the code in the slice file.
227227

228-
The `<PostsList>` component needs to read a list of all the posts, and the `<SinglePostPage>` and `<EditPostForm>` components need to look up a single post by its ID. Let's export two small selector functions from `postsSlice.js` to cover those cases:
228+
#### Defining Selector Functions
229+
230+
You've already been writing selector functions every time we called `useAppSelector`, such as `useAppSelector( state => state.posts )`. In that case, the selector is being defined inline. Since it's just a function, we could also write it as:
231+
232+
```ts
233+
const selectPosts = (state: RootState) => state.posts
234+
const posts = useAppSelector(selectPosts)
235+
```
236+
237+
Selectors are typically written as standalone individual functions in a slice file. They normally accept the entire Redux `RootState` as the first argument, and may also accept other arguments as well.
238+
239+
#### Writing Posts Selectors
240+
241+
The `<PostsList>` component needs to read a list of all the posts, and the `<SinglePostPage>` and `<EditPostForm>` components need to look up a single post by its ID. Let's export two small selector functions from `postsSlice.ts` to cover those cases:
242+
243+
```ts title="features/posts/postsSlice.ts"
244+
import type { RootState } from '@/app/store'
229245

230-
```js title="features/posts/postsSlice.js"
231246
const postsSlice = createSlice(/* omit slice code*/)
232247

233248
export const { postAdded, postUpdated, reactionAdded } = postsSlice.actions
234249

235250
export default postsSlice.reducer
236251

237252
// highlight-start
238-
export const selectAllPosts = state => state.posts
253+
export const selectAllPosts = (state: RootState) => state.posts
239254

240-
export const selectPostById = (state, postId) =>
255+
export const selectPostById = (state: RootState, postId: string) =>
241256
state.posts.find(post => post.id === postId)
242257
//highlight-end
243258
```
244259

245-
Note that the `state` parameter for these selector functions is the root Redux state object, as it was for the inlined anonymous selectors we wrote directly inside of `useSelector`.
260+
Note that the `state` parameter for these selector functions is the root Redux state object, as it was for the inlined anonymous selectors we wrote directly inside of `useAppSelector`.
246261

247262
We can then use them in the components:
248263

249-
```js title="features/posts/PostsList.js"
264+
```tsx title="features/posts/PostsList.tsx"
250265
// omit imports
251266
// highlight-next-line
252267
import { selectAllPosts } from './postsSlice'
253268

254269
export const PostsList = () => {
255270
// highlight-next-line
256-
const posts = useSelector(selectAllPosts)
271+
const posts = useAppSelector(selectAllPosts)
257272
// omit component contents
258273
}
259274
```
260275

261-
```js title="features/posts/SinglePostPage.js"
276+
```tsx title="features/posts/SinglePostPage.tsx"
262277
// omit imports
263278
//highlight-next-line
264279
import { selectPostById } from './postsSlice'
265280

266-
export const SinglePostPage = ({ match }) => {
267-
const { postId } = match.params
281+
export const SinglePostPage = () => {
282+
const { postId } = useParams()
268283

269284
// highlight-next-line
270-
const post = useSelector(state => selectPostById(state, postId))
285+
const post = useAppSelector(state => selectPostById(state, postId!))
271286
// omit component logic
272287
}
273288
```
@@ -286,10 +301,116 @@ export const EditPostForm = ({ match }) => {
286301
}
287302
```
288303

289-
It's often a good idea to encapsulate data lookups by writing reusable selectors. You can also create "memoized" selectors that can help improve performance, which we'll look at in a later part of this tutorial.
304+
#### Extracting Auth and Users Selectors
305+
306+
While we're at it, we also have several more components that have inlined selectors for accessing `state.auth` and `state.users`. That includes multiple components that are checking the current logged-in username or getting the current user object.
307+
308+
We can extract those into reusable selectors in their respective slices as well:
309+
310+
```ts title="features/auth/authSlice.ts"
311+
export default authSlice.reducer
312+
313+
// highlight-next-line
314+
export const selectCurrentUsername = (state: RootState) => state.auth.username
315+
```
316+
317+
```ts title="features/users/usersSlice.ts"
318+
// highlight-start
319+
import type { RootState } from '@/app/store'
320+
321+
import { selectCurrentUsername } from '../auth/authSlice'
322+
// highlight-end
323+
324+
// omit slice definition
325+
326+
export default usersSlice.reducer
327+
328+
// highlight-start
329+
export const selectAllUsers = (state: RootState) => state.users
330+
331+
export const selectUserById = (state: RootState, userId?: string) => {
332+
return state.users.find(user => user.id === userId)
333+
}
334+
335+
export const selectCurrentUser = (state: RootState) => {
336+
const currentUsername = selectCurrentUsername(state)
337+
if (currentUsername) {
338+
return selectUserById(state, currentUsername)
339+
}
340+
}
341+
// highlight-end
342+
```
343+
344+
Notice that `selectCurrentUser` actually makes use of the `selectCurrentUsername` selector from the auth slice! Since selectors are just normal functions, they can call each other to look up necessary pieces of data from the state.
345+
346+
Once we've written these new selectors, we can replace all of the remaining inlined selectors in our components with the matching selectors from the slice files.
347+
348+
#### Using Selectors Effectively
349+
350+
It's often a good idea to encapsulate data lookups by writing reusable selectors. Ideally, components don't even have to know where in the Redux `state` a value lives - they just use a selector from the slice to access the data.
351+
352+
You can also create "memoized" selectors that can help improve performance by optimizing rerenders and skipping unnecessary recalculations, which we'll look at in a later part of this tutorial.
290353

291354
But, like any abstraction, it's not something you should do _all_ the time, everywhere. Writing selectors means more code to understand and maintain. **Don't feel like you need to write selectors for every single field of your state**. Try starting without any selectors, and add some later when you find yourself looking up the same values in many parts of your application code.
292355

356+
#### Optional: Defining Selectors Inside of `createSlice`
357+
358+
We've seen that we can write selectors as standalone functions in slice files. In some cases, you can shorten this a bit by defining selectors directly inside `createSlice` itself.
359+
360+
<DetailedExplanation title="Defining Selectors inside createSlice" >
361+
362+
We've already seen that `createSlice` requires the `name`, `initialState`, and `reducers` fields, and also accepts an optional `extraReducers` field.
363+
364+
If you want to define selectors directly inside of `createSlice`, you can pass in an additional `selectors` field. The `selectors` field should be an object similar to `reducers`, where the keys will be the selector function names, and the values are the selector functions to be generated.
365+
366+
**Note that unlike writing a standalone selector function, the `state` argument to these selectors will be just the _slice state_, and _not_ the entire `RootState`!**.
367+
368+
There _are_ still times you'll need to write selectors as standalone functions outside of `createSlice`. This is especially true if you're calling other selectors that need the entire `RootState` as their argument, in order to make sure the types match up correctly.
369+
370+
Here's what it might look like to convert the users slice selectors to be defined inside of `createSlice`:
371+
372+
```ts
373+
const usersSlice = createSlice({
374+
name: 'users',
375+
initialState,
376+
reducers: {},
377+
// highlight-start
378+
selectors: {
379+
// Note that `state` here is just the `UsersState`!
380+
selectAllUsers: state => state,
381+
selectUserById: (state, userId?: string) => {
382+
return state.find(user => user.id === userId)
383+
}
384+
}
385+
// highlight-end
386+
})
387+
388+
export const { selectAllUsers, selectUserById } = usersSlice.selectors
389+
390+
export default usersSlice.reducer
391+
392+
// highlight-start
393+
// We've replaced these standalone selectors:
394+
// export const selectAllUsers = (state: RootState) => state.users
395+
396+
// export const selectUserById = (state: RootState, userId?: string) => {
397+
// return state.users.find((user) => user.id === userId)
398+
// }
399+
400+
// But this selector still needs to be written standalone,
401+
// because `selectCurrentUsername` is typed to need `RootState`
402+
// as its argument:
403+
export const selectCurrentUser = (state: RootState) => {
404+
const currentUsername = selectCurrentUsername(state)
405+
if (currentUsername) {
406+
return selectUserById(state, currentUsername)
407+
}
408+
}
409+
// highlight-end
410+
```
411+
412+
</DetailedExplanation>
413+
293414
### Loading State for Requests
294415

295416
When we make an API call, we can view its progress as a small state machine that can be in one of four possible states:
@@ -641,7 +762,7 @@ const ARTIFICIAL_DELAY_MS = 2000
641762

642763
Feel free to turn that on and off as we go if you want the API calls to complete faster.
643764

644-
### [TODO] Defining Thunks Inside of `createSlice`
765+
### [TODO] Optional: Defining Thunks Inside of `createSlice`
645766

646767
## Loading Users
647768

0 commit comments

Comments
 (0)