Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revamp "Redux Essentials" tutorial to be TS-first and update contents #4706

Merged
merged 42 commits into from
Aug 1, 2024

Conversation

markerikson
Copy link
Contributor

@markerikson markerikson commented May 12, 2024

Actual content changes for #4393 , at long last!

The current WIP example code is over in:

I'm doing another round of revisions and step-by-step checking to those code commits as I rework the tutorial content, but that should be the progression and code content I want to show off in the tutorial.

Big picture summary:

  • The entire example app is TS, start to finish
  • Improve some existing explanations
  • Cover some concepts that weren't included originally, like having multiple reducers handle one action
  • Cover newer APIs we didn't have in 2020, like createListenerMiddleware and thunks in createSlice
  • While not Redux-related, update some of the usage patterns like React Router 5 -> 6 and using uncontrolled inputs in forms instead of controlled

Copy link

codesandbox-ci bot commented May 12, 2024

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Copy link

netlify bot commented May 12, 2024

Deploy Preview for redux-docs ready!

Name Link
🔨 Latest commit 41e746b
🔍 Latest deploy log https://app.netlify.com/sites/redux-docs/deploys/66abd84068ae1400081df970
😎 Deploy Preview https://deploy-preview-4706--redux-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

```ts title="features/posts/postsSlice.ts"
// highlight-start
// Import the `PayloadAction` TS type
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
import { createSlice } from '@reduxjs/toolkit'

Explicit type imports could help better distinguish between TS/JS related material on a quick glance.

```js title="features/posts/postsSlice.js"
```ts title="features/posts/postsSlice.ts"
// highlight-next-line
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
import { createSlice } from '@reduxjs/toolkit'

```js title="features/users/usersSlice.js"
import { createSlice } from '@reduxjs/toolkit'
```ts title="features/users/usersSlice.ts"
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
import { createSlice } from '@reduxjs/toolkit'

```jsx title="features/posts/ReactionButtons.js"
import React from 'react'
```ts
import { createSlice, nanoid, PayloadAction } from '@reduxjs/toolkit'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import { createSlice, nanoid, PayloadAction } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
import { createSlice, nanoid } from '@reduxjs/toolkit'

In this case, our auth state is really just the current logged-in username, and we'll reset it to `null` if they log out.

```ts title="features/auth/authSlice.ts"
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
import { createSlice } from '@reduxjs/toolkit'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may apply the type import change near the end, but I'm honestly not worried about that for now

Given that, we can import the `userLoggedOut` action from `authSlice.ts` into `postsSlice.ts`, listen for that action inside of `postsSlice.extraReducers`, and return an empty posts array to reset the posts list on logout:

```ts title="features/posts/postsSlice.ts"
import { createSlice, nanoid, PayloadAction } from '@reduxjs/toolkit'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import { createSlice, nanoid, PayloadAction } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
import { createSlice, nanoid } from '@reduxjs/toolkit'

@Dan503
Copy link

Dan503 commented Jul 30, 2024

Implementing Streaming Updates

This line needs to be highlighted:

const notificationsReceived = createAction<ServerNotification[]>('notifications/notificationsReceived')

I didn't see that I needed to add it, then I ran into an error when forming the matchNotificationsReceived variable.

@Dan503
Copy link

Dan503 commented Jul 30, 2024

Implementing Streaming Updates

const message: {
  type: 'notifications'
  payload: ServerNotification[]
} = JSON.parse(event.data)

I'm not sure why you are making a custom type for the message variable inside listener() when you could use PayloadAction<ServerNotification[]> as the type instead.

const message: PayloadAction<ServerNotification[]> = JSON.parse(event.data)

@Dan503
Copy link

Dan503 commented Jul 30, 2024

Implementing Streaming Updates

catch {
  // no-op in case `cacheEntryRemoved` resolves before `cacheDataLoaded`,
  // in which case `cacheDataLoaded` will throw
}

The wording on this error handler comment is confusing.

This is my understanding of that comment:

Ignore any errors caused by the cache entry being removed (cacheEntryRemoved) before the cache data has finished loading (cacheDataLoaded)

@Dan503
Copy link

Dan503 commented Jul 30, 2024

I've completed the tutorial now. Due to the error, I can't tell if I've completed it successfully or not. The new tutorial was much easier to follow though and I felt far less intimidated working through it this time.

The web-socket portion is still complex. The comments throughout that code block go a long way to walking the reader through that bit of code though. The code-block sizes are much more reasonable now as well. I felt guided through the tutorial this time which is a huge improvement.

@markerikson
Copy link
Contributor Author

markerikson commented Jul 30, 2024

If it isn't supposed to be functional yet then the wording at the end of that section needs to be updated:

@Dan503 : Ah, yeah, the issue is that it isn't supposed to be functional yet. The websocket connection doesn't get activated until the section after this, so it's expected that the button is actually "broken" at this point in the sequence. It still needs the onCacheEntryAdded logic from the final commit / instruction sequence, which is where the Websocket itself is getting created and told to connect to the mock server.

I can try to clarify the instructions here, as well as maybe add an alert or log message if the socket doesn't exist yet.

@markerikson
Copy link
Contributor Author

I'm not sure why you are making a custom type for the message variable inside listener() when you could use PayloadAction<ServerNotification[]> as the type instead.

Mostly because I wasn't thinking of it that way :)

The mock server backend could send any arbitrary data over the socket. I just happened to structure its message similarly to how a Redux action looks out of habit. Since I wasn't thinking of it as an actual action, it didn't occur to me to reuse PayloadAction here.

@Dan503
Copy link

Dan503 commented Jul 30, 2024

@markerikson

It still needs the onCacheEntryAdded logic from the final commit / instruction sequence, which is where the Websocket itself is getting created and told to connect to the mock server.

Ok... I was still getting the error by the time I reached the end of the tutorial though 😕
The problem should have fixed itself when I reached the end if that is the only issue.

My code at the end of the tutorial

@markerikson
Copy link
Contributor Author

@Dan503 : checked out your code. I don't see the Cannot read properties of undefined (reading 'send') error, and logging shows that the websocket is getting set up correctly.

I suspect that the issue might have something to do with hot module reloading, but I'm not entirely sure.

Behavior-wise, I see a bug in the onCacheEntryAdded handler - the lifecycleApi.updateCachedData thunk needs to be dispatched, and it isn't, so the new notification entries from the socket aren't actually getting added to the store.

So, I think the example repo code and instructions are correct at this point, but I'll give them another look-through.

@Dan503
Copy link

Dan503 commented Jul 31, 2024

Behavior-wise, I see a bug in the onCacheEntryAdded handler - the lifecycleApi.updateCachedData thunk needs to be dispatched, and it isn't, so the new notification entries from the socket aren't actually getting added to the store.

@markerikson I'm not sure what you mean by that.

The code written in the tutorial doesn't wrap lifecycleApi.updateCachedData() in a dispatch function.

It writes it like this:

switch (message.type) {
   case 'notifications': {
      lifecycleApi.updateCachedData((draft) => {
         // Insert all received notifications from the websocket
         // into the existing RTKQ cache array
         draft.push(...message.payload)
         draft.sort((a, b) => b.date.localeCompare(a.date))
      })
      break
   }
   default:
      break
}

My code:

if (message.type === 'notifications') {
   lifecycleApi.updateCachedData((draft) => {
      // Insert all received notifications from the websocket
      // into the existing RTKQ cache array
      draft.push(...message.payload)
      draft.sort((a, b) => b.date.localeCompare(a.date))
   })
}

@markerikson
Copy link
Contributor Author

Ah, yeah. Sorry, getting things mixed up :) apiSlice.utils.updateCachedData requires all the params and a separate dispatch, lifecycleApi.updateCachedDate does not because it has those wrapped in already.

I'll have to look further to see why your code isn't getting the notifications saved right. I see the websocket messages going through...

@markerikson
Copy link
Contributor Author

markerikson commented Jul 31, 2024

Ah, I see it! The notifications are getting added fine, but onCacheEntryAdded is missing this:

 				// Dispatch an additional action so we can track "read" state
                lifecycleApi.dispatch(notificationsReceived(message.payload))
                break

so the extraReducers code isn't running and we don't get the metadata items added for the notification entries.

So, to recap:

  • I didn't see the Cannot read properties of undefined (reading 'send') when I cloned and read your code, and I think that's maybe related to hot module reloading while editing code
  • The "unread notifications" count wasn't getting updated when I hit "Refresh Notifications" with the websocket, and it's because we need the extra action to be dispatched to trigger the reducer logic

@Dan503
Copy link

Dan503 commented Jul 31, 2024

I noticed a bug in my code. I pushed up a fix for it.

I didn't store the current user correctly when logging in so currentUser on the server side was always undefined. That might have been causing some issues.

@Dan503
Copy link

Dan503 commented Jul 31, 2024

@markerikson I've updated my listener code to this and it's working now :)

// when data is received from the socket connection to the server,
// update our query result with the received message
function listener(event: MessageEvent<string>) {
   const message: PayloadAction<Array<ServerNotification>> = JSON.parse(
      event.data,
   )
   if (message.type === 'notifications') {
      lifecycleApi.updateCachedData((draft) => {
         // Insert all received notifications from the websocket
         // into the existing RTKQ cache array
         draft.push(...message.payload)
         draft.sort((a, b) => b.date.localeCompare(a.date))
      })

      // Dispatch an additional action so we can track "read" state
      lifecycleApi.dispatch(notificationsReceived(message.payload))
   }
}

So the instructions were missing this line of code

   // Dispatch an additional action so we can track "read" state
   lifecycleApi.dispatch(notificationsReceived(message.payload))
 break

@markerikson
Copy link
Contributor Author

Yep, my bad, I see I did forget to include those lines there! Updating that locally.

@Dan503
Copy link

Dan503 commented Jul 31, 2024

So the instruction clarification

I can try to clarify the instructions here, as well as maybe add an alert or log message if the socket doesn't exist yet.

And the notificationsReceived dispatch

lifecycleApi.dispatch(notificationsReceived(message.payload))

Are the only two changes that the documentation needs (along with any "TODO" items you may have left for applying images or other things like that)

@Dan503
Copy link

Dan503 commented Jul 31, 2024

Oh also the highlighting of the following line of code as mentioned in #4706 (comment)

const notificationsReceived = createAction<ServerNotification[]>('notifications/notificationsReceived')

That is a small though important change or it will confuse a huge number developers doing the tutorial.

@markerikson
Copy link
Contributor Author

Okay, I think this is done!

  • Updated all screenshots
  • Fixed the missing bits of code in Part 8
  • Added an alert from server.ts if the socket isn't initialized yet
  • All sandboxes should be working. They're currently pointing to fixed commit IDs - I'll point them to the final tags once I switch the branches in the Essentials example repo

I'm sure there's leftover typos and things that could be improved, but I think this is good enough to merge and launch at this point!

@Dan503 THANK YOU SO MUCH FOR YOUR DETAILED REVIEW FEEDBACK! You made this drastically better!

@Dan503
Copy link

Dan503 commented Jul 31, 2024

@markerikson the timing ended up working out ridiculously well... like insanely well...

  1. I'm in-between projects at work at the moment so my only task is self-directed study
  2. I chose Redux as one of my study topics because I am using it in a side project and I only knew the basics, I wanted to know it in depth
  3. The portability bug hadn't been fixed yet so I encountered it when trying to do the tutorial in TS on my own (which lead me to that original github issue)
  4. None of the existing answers were working for me so I created my own solution and posted what I did to help future developers (so if any of the other answers worked for me this feedback wouldn't have happened)
  5. The documentation for the TS version was already almost complete, just not published yet, allowing me to redo the full tutorial in TS with the correct types info this time
  6. Since the tutorial was in PR stage and not published yet (and you explicitly asked for feedback) I felt empowered to comment. (If it had already been published I probably wouldn't have said anything)
  7. Being in-between projects at work gave me the time needed to redo the tutorial and provide feedback and this seems like something important for the betterment of the wider web development community so I set it as a high priority

So yeah, a lot of converging coincidences to make this thing happen 🤯

@markerikson
Copy link
Contributor Author

Okay. The app repo at https://github.com/reduxjs/redux-essentials-example-app now has master pointing to the initial TS app setup commit, and has the full tutorial-steps-ts branch available. I added tags to the last commit of each section. I've updated all the sandbox links to point to those tags.

LET'S DO THIS!

For the record, some word count stats:

Existing Content

  1. 3279
  2. 5299
  3. 3107
  4. 5197
  5. 5831
  6. 6526
  7. 5986
  8. 8292

Total: 43517

Revised Content

  1. 3318
  2. 6498
  3. 4851
  4. 9280
  5. 7316
  6. 9910
  7. 7017
  8. 11185

Total: 52358

@markerikson markerikson merged commit 42cc13b into master Aug 1, 2024
18 checks passed
@markerikson markerikson deleted the docs/essentials-revamp-ts branch August 1, 2024 18:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants