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

fix(closes #527): Enable cow renaming #528

Merged
merged 5 commits into from
Dec 1, 2024
Merged

Conversation

jeremyckahn
Copy link
Owner

What this PR does

This PR fixes #527, a regression that prevented players from giving their cows custom names.

How this change can be validated

Successfully rename a cow (this is only possible with cows that were not received via trade).

Questions or concerns about this change

I have no idea how long this was broken for. 😭

@jeremyckahn jeremyckahn requested a review from lstebner November 29, 2024 18:13
Copy link

vercel bot commented Nov 29, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
farmhand ✅ Ready (Inspect) Visit Preview 💬 Add feedback Dec 1, 2024 4:46pm

@@ -66,7 +66,7 @@ describe('Inventory Component', () => {
const searchInput = screen.getByPlaceholderText('Search inventory...')
fireEvent.change(searchInput, { target: { value: 'Carrot' } })

vitest.runAllTimers()
vitest.advanceTimersByTime(1000)
Copy link
Owner Author

Choose a reason for hiding this comment

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

This improves a flakey test introduced by #526.

@@ -11,7 +11,7 @@ export const useMountState = () => {
}
}, [isMountedRef])

const isMounted = () => isMountedRef.current
const isMounted = useCallback(() => isMountedRef.current, [isMountedRef])
Copy link
Owner Author

@jeremyckahn jeremyckahn Nov 29, 2024

Choose a reason for hiding this comment

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

This fixes the root cause of #527.

Specifically, the fact that this wasn't wrapped in a useCallback meant that this useEffect would run on every render and reset the cow name (see line 137):

useEffect(() => {
;(async () => {
const cowImage = await getCowImage(cow)
if (isMounted() === false) return
setCowImage(cowImage)
})()
setDisplayName(getCowDisplayName(cow, id, allowCustomPeerCowNames))
}, [cow, id, allowCustomPeerCowNames, isMounted])

Copy link
Collaborator

Choose a reason for hiding this comment

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

using a ref in this code is kind of strange to me. if you like the ref use this isn't a big deal but i would have written this hook like:

const [isMounted, setIsMounted] = useState(false)

useEffect(() => setIsMounted(true), [])

return { isMounted }

in general though having isMounted checks is a code smell for me so it may be more
worthwhile to try to refactor all the places this hook is needed than to worry about rewriting it right now.

Copy link
Owner Author

@jeremyckahn jeremyckahn Dec 1, 2024

Choose a reason for hiding this comment

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

I'm glad you raised this because it made me realize that #526 introduced the inclusion of https://usehooks-ts.com/. This library provides an implementation of useIsMounted which effectively abstracts away the contents of this hook. Ironically the library implementation seems equivalent to what we have here, but since it's in a library we don't have to worry too much about the implementation details so long as it works (which it does).

I've switched to using the usehooks-ts implementation in 72d110b to eliminate the maintenance burden of our custom one.

in general though having isMounted checks is a code smell for me so it may be more
worthwhile to try to refactor all the places this hook is needed than to worry about rewriting it right now.

I generally agree and try to avoid using isMounted checks for this reason. In this case it was necessary for Cow image generation:

;(async () => {
const cowImage = await getCowImage(cow)
if (isMounted() === false) return
setCowImage(cowImage)
})()

In this part of the code, the Cow image is generated dynamically as an asynchronous operation. By the time the image generation is complete, it's possible that the component has unmounted which would mean setCowImage(cowImage) would be improperly setting state on an unmounted component. There are probably cleaner abstractions for this sort of thing, but this seemed like the simplest solution. I don't know that we'd want to repeat this pattern too much, but I think this is the only part of the code base that we do this right now so I think that this bit of isolated awkwardness is acceptable.

Copy link
Collaborator

Choose a reason for hiding this comment

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

oh that's great we have a library version of this to use instead! also if it's only used for that one use case that's not a big deal at all. i did not look around for the uses

@@ -46,7 +46,7 @@
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */

/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
"moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
Copy link
Owner Author

Choose a reason for hiding this comment

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

This seems to make type checking work better.

Copy link
Collaborator

@lstebner lstebner left a comment

Choose a reason for hiding this comment

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

leaving some thoughts but i'm also ok with a merge to get the bug fixed. i don't need to hold that up over semantics 😄

userEvent.clear(nameInput)
userEvent.type(nameInput, customName)

await waitFor(() => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

i think you should be able to await the userEvent.type calls and avoid waitFor entirely

Copy link
Owner Author

Choose a reason for hiding this comment

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

i think you should be able to await the userEvent.type calls and avoid waitFor entirely

It turns out that clear and type only return Promises in @testing-library/react v14. Farmhand currently uses v13, and upgrading to v14 breaks a number of other tests so it's not worth making that change in a narrowly-scope bugfix PR such as this.

However, it appears that the await waitFor(...) isn't even necessary here (I think it was for an earlier iteration), so I've removed it in 9601f41. Thanks for bringing this up helping to simplify this part of the code!

Copy link
Collaborator

Choose a reason for hiding this comment

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

ah right, i forgot the switch to async was in later versions

@@ -11,7 +11,7 @@ export const useMountState = () => {
}
}, [isMountedRef])

const isMounted = () => isMountedRef.current
const isMounted = useCallback(() => isMountedRef.current, [isMountedRef])
Copy link
Collaborator

Choose a reason for hiding this comment

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

using a ref in this code is kind of strange to me. if you like the ref use this isn't a big deal but i would have written this hook like:

const [isMounted, setIsMounted] = useState(false)

useEffect(() => setIsMounted(true), [])

return { isMounted }

in general though having isMounted checks is a code smell for me so it may be more
worthwhile to try to refactor all the places this hook is needed than to worry about rewriting it right now.

Copy link
Owner Author

@jeremyckahn jeremyckahn left a comment

Choose a reason for hiding this comment

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

Thanks for the review @lstebner! The code is looking better with your feedback incorporated, as always. 🙂

I'll merge this now!

@@ -11,7 +11,7 @@ export const useMountState = () => {
}
}, [isMountedRef])

const isMounted = () => isMountedRef.current
const isMounted = useCallback(() => isMountedRef.current, [isMountedRef])
Copy link
Owner Author

@jeremyckahn jeremyckahn Dec 1, 2024

Choose a reason for hiding this comment

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

I'm glad you raised this because it made me realize that #526 introduced the inclusion of https://usehooks-ts.com/. This library provides an implementation of useIsMounted which effectively abstracts away the contents of this hook. Ironically the library implementation seems equivalent to what we have here, but since it's in a library we don't have to worry too much about the implementation details so long as it works (which it does).

I've switched to using the usehooks-ts implementation in 72d110b to eliminate the maintenance burden of our custom one.

in general though having isMounted checks is a code smell for me so it may be more
worthwhile to try to refactor all the places this hook is needed than to worry about rewriting it right now.

I generally agree and try to avoid using isMounted checks for this reason. In this case it was necessary for Cow image generation:

;(async () => {
const cowImage = await getCowImage(cow)
if (isMounted() === false) return
setCowImage(cowImage)
})()

In this part of the code, the Cow image is generated dynamically as an asynchronous operation. By the time the image generation is complete, it's possible that the component has unmounted which would mean setCowImage(cowImage) would be improperly setting state on an unmounted component. There are probably cleaner abstractions for this sort of thing, but this seemed like the simplest solution. I don't know that we'd want to repeat this pattern too much, but I think this is the only part of the code base that we do this right now so I think that this bit of isolated awkwardness is acceptable.

userEvent.clear(nameInput)
userEvent.type(nameInput, customName)

await waitFor(() => {
Copy link
Owner Author

Choose a reason for hiding this comment

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

i think you should be able to await the userEvent.type calls and avoid waitFor entirely

It turns out that clear and type only return Promises in @testing-library/react v14. Farmhand currently uses v13, and upgrading to v14 breaks a number of other tests so it's not worth making that change in a narrowly-scope bugfix PR such as this.

However, it appears that the await waitFor(...) isn't even necessary here (I think it was for an earlier iteration), so I've removed it in 9601f41. Thanks for bringing this up helping to simplify this part of the code!

@@ -123,7 +123,7 @@ export const CowCard = (
isCowOfferedForTradeByPeer && cowIdOfferedForTrade.length > 0
)

const { isMounted } = useMountState()
const isMounted = useIsMounted()
Copy link
Owner Author

Choose a reason for hiding this comment

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

As of 72d110b, this is now the solution to the root cause of #527.

@jeremyckahn jeremyckahn merged commit dd084d2 into develop Dec 1, 2024
5 checks passed
@jeremyckahn jeremyckahn deleted the bugfix/527__cow-renaming branch December 1, 2024 16:52
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.

Cow renaming is broken
2 participants