Use useEffectEvent for effects with deliberately narrow deps
`pnpm lint` runs oxlint with `--quiet`, which hides warnings, so 14 react-hooks/exhaustive-deps warnings were sitting unseen. Five are the shape useEffectEvent exists for: an effect that must read the latest value of something without re-running when it changes. - CustomFeedEmptyState, FindContactsSettings: fire-and-forget metrics that omitted `ax` from the dep array - AutosizedTextarea: the `onUpdateHeight` callback prop - SuggestedLanguage: replaces a `useNonReactiveObject` mirror, which was doing the same job by hand - VideoEmbedInnerWeb: the hls subtitle handler called a captured `updateCuePositions`, so it would have gone stale if that identity changed Warnings: 14 -> 9. The rest are useMemo/useCallback/useImperativeHandle, where useEffectEvent does not apply. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,11 @@
|
|||||||
import {useCallback, useEffect, useId, useRef, useState} from 'react'
|
import {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useEffectEvent,
|
||||||
|
useId,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {useLingui} from '@lingui/react/macro'
|
import {useLingui} from '@lingui/react/macro'
|
||||||
import type * as HlsTypes from 'hls.js'
|
import type * as HlsTypes from 'hls.js'
|
||||||
@@ -264,6 +271,14 @@ function useHLS({
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The hls handler below must call the latest `updateCuePositions` without the
|
||||||
|
* effect tearing down and re-attaching every time its identity changes.
|
||||||
|
*/
|
||||||
|
const onSubtitleFragProcessed = useEffectEvent(() => {
|
||||||
|
updateCuePositions()
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!videoRef.current) return
|
if (!videoRef.current) return
|
||||||
if (!Hls) return
|
if (!Hls) return
|
||||||
@@ -302,7 +317,7 @@ function useHLS({
|
|||||||
})
|
})
|
||||||
|
|
||||||
hls.on(Hls.Events.SUBTITLE_FRAG_PROCESSED, () => {
|
hls.on(Hls.Events.SUBTITLE_FRAG_PROCESSED, () => {
|
||||||
updateCuePositions()
|
onSubtitleFragProcessed()
|
||||||
})
|
})
|
||||||
|
|
||||||
hls.on(Hls.Events.FRAG_BUFFERED, (_event, {frag}) => {
|
hls.on(Hls.Events.FRAG_BUFFERED, (_event, {frag}) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {useEffect, useMemo, useRef, useState} from 'react'
|
import {useEffect, useEffectEvent, useMemo, useRef, useState} from 'react'
|
||||||
import {
|
import {
|
||||||
TextInput,
|
TextInput,
|
||||||
type TextInputContentSizeChangeEvent,
|
type TextInputContentSizeChangeEvent,
|
||||||
@@ -141,13 +141,17 @@ export function AutosizedTextarea({
|
|||||||
* Reset native height state after a programmatic clear. Android uses it as
|
* Reset native height state after a programmatic clear. Android uses it as
|
||||||
* the explicit input height, while iOS uses it to decide when to scroll.
|
* the explicit input height, while iOS uses it to decide when to scroll.
|
||||||
*/
|
*/
|
||||||
|
const reportHeight = useEffectEvent((height: number) => {
|
||||||
|
onUpdateHeight?.(height)
|
||||||
|
})
|
||||||
|
|
||||||
const prevRawValue = useRef(rawValue || '')
|
const prevRawValue = useRef(rawValue || '')
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!IS_NATIVE) return
|
if (!IS_NATIVE) return
|
||||||
if (rawValue === undefined) return // uncontrolled
|
if (rawValue === undefined) return // uncontrolled
|
||||||
if (prevRawValue.current?.length && rawValue === '') {
|
if (prevRawValue.current?.length && rawValue === '') {
|
||||||
setNativeHeight(minInputHeight)
|
setNativeHeight(minInputHeight)
|
||||||
onUpdateHeight?.(minInputHeight)
|
reportHeight(minInputHeight)
|
||||||
}
|
}
|
||||||
prevRawValue.current = rawValue
|
prevRawValue.current = rawValue
|
||||||
}, [rawValue, minInputHeight])
|
}, [rawValue, minInputHeight])
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {useCallback, useEffect, useState} from 'react'
|
import {useCallback, useEffect, useEffectEvent, useState} from 'react'
|
||||||
import {type ListRenderItemInfo, View} from 'react-native'
|
import {type ListRenderItemInfo, View} from 'react-native'
|
||||||
import * as Contacts from 'expo-contacts'
|
import * as Contacts from 'expo-contacts'
|
||||||
import {type DidString} from '@atproto/syntax'
|
import {type DidString} from '@atproto/syntax'
|
||||||
@@ -61,12 +61,17 @@ export function FindContactsSettingsScreen({}: Props) {
|
|||||||
const {data, error, refetch} = useContactsSyncStatusQuery()
|
const {data, error, refetch} = useContactsSyncStatusQuery()
|
||||||
|
|
||||||
const isFocused = useIsFocused()
|
const isFocused = useIsFocused()
|
||||||
|
|
||||||
|
const logPresented = useEffectEvent(() => {
|
||||||
|
ax.metric('contacts:settings:presented', {
|
||||||
|
hasPreviouslySynced: !!data?.syncStatus,
|
||||||
|
matchCount: data?.syncStatus?.matchesCount,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data && isFocused) {
|
if (data && isFocused) {
|
||||||
ax.metric('contacts:settings:presented', {
|
logPresented()
|
||||||
hasPreviouslySynced: !!data.syncStatus,
|
|
||||||
matchCount: data.syncStatus?.matchesCount,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}, [data, isFocused])
|
}, [data, isFocused])
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {useEffect, useMemo, useRef, useState} from 'react'
|
import {useEffect, useEffectEvent, useMemo, useRef, useState} from 'react'
|
||||||
import {Platform, Text as RNText, View} from 'react-native'
|
import {Platform, Text as RNText, View} from 'react-native'
|
||||||
import {parseLanguageString} from '@atproto/syntax'
|
import {parseLanguageString} from '@atproto/syntax'
|
||||||
import {
|
import {
|
||||||
@@ -332,15 +332,18 @@ function GuessedLanguage({
|
|||||||
onDeclineOuter()
|
onDeclineOuter()
|
||||||
}
|
}
|
||||||
|
|
||||||
const metaRef = useNonReactiveObject(metadata)
|
const logSuggestion = useEffectEvent(() => {
|
||||||
useEffect(() => {
|
|
||||||
ax.metric('composer:language:suggestLanguage', {
|
ax.metric('composer:language:suggestLanguage', {
|
||||||
os: Platform.OS,
|
os: Platform.OS,
|
||||||
suggestedLanguage: language,
|
suggestedLanguage: language,
|
||||||
currentTargetLanguages: metaRef.current.currentTargetLanguages,
|
currentTargetLanguages: metadata.currentTargetLanguages,
|
||||||
textLength: sanitizeTextForDetection(metadata.rawText).length,
|
textLength: sanitizeTextForDetection(metadata.rawText).length,
|
||||||
})
|
})
|
||||||
}, [ax, language])
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
logSuggestion()
|
||||||
|
}, [language])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LanguageSuggestionButton
|
<LanguageSuggestionButton
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {useCallback, useEffect, useRef} from 'react'
|
import {useCallback, useEffect, useEffectEvent, useRef} from 'react'
|
||||||
import {StyleSheet, View} from 'react-native'
|
import {StyleSheet, View} from 'react-native'
|
||||||
import {Trans, useLingui} from '@lingui/react/macro'
|
import {Trans, useLingui} from '@lingui/react/macro'
|
||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
@@ -23,6 +23,11 @@ export function CustomFeedEmptyState() {
|
|||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const hasLoggedDiscoverEmptyErrorRef = useRef(false)
|
const hasLoggedDiscoverEmptyErrorRef = useRef(false)
|
||||||
|
|
||||||
|
const logDiscoverEmptyError = useEffectEvent((did: string) => {
|
||||||
|
hasLoggedDiscoverEmptyErrorRef.current = true
|
||||||
|
ax.metric('feed:discover:emptyError', {userDid: did})
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Log the empty feed error event
|
// Log the empty feed error event
|
||||||
if (feedFeedback.feedSourceInfo && currentAccount?.did) {
|
if (feedFeedback.feedSourceInfo && currentAccount?.did) {
|
||||||
@@ -31,10 +36,7 @@ export function CustomFeedEmptyState() {
|
|||||||
uri === DISCOVER_FEED_URI &&
|
uri === DISCOVER_FEED_URI &&
|
||||||
!hasLoggedDiscoverEmptyErrorRef.current
|
!hasLoggedDiscoverEmptyErrorRef.current
|
||||||
) {
|
) {
|
||||||
hasLoggedDiscoverEmptyErrorRef.current = true
|
logDiscoverEmptyError(currentAccount.did)
|
||||||
ax.metric('feed:discover:emptyError', {
|
|
||||||
userDid: currentAccount.did,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [feedFeedback.feedSourceInfo, currentAccount?.did])
|
}, [feedFeedback.feedSourceInfo, currentAccount?.did])
|
||||||
|
|||||||
Reference in New Issue
Block a user