Unblock React Compiler for 18 components with value blocks inside try (#11548)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tomasz Zawadzki
2026-08-31 15:47:42 +02:00
committed by GitHub
parent 80d09a242d
commit 8bf5696d3c
18 changed files with 179 additions and 85 deletions
+12 -6
View File
@@ -73,12 +73,18 @@ export function Outer({
setIsOpen(false)
try {
if (cb && typeof cb === 'function') {
// This timeout ensures that the callback runs at the same time as it would on native. I.e.
// console.log('Step 1') -> close(() => console.log('Step 3')) -> console.log('Step 2')
// This should always output 'Step 1', 'Step 2', 'Step 3', but without the timeout it would output
// 'Step 1', 'Step 3', 'Step 2'.
setTimeout(cb)
/*
* Nested rather than `&&`: React Compiler cannot lower a logical
* expression in a test position inside a `try`.
*/
if (cb) {
if (typeof cb === 'function') {
// This timeout ensures that the callback runs at the same time as it would on native. I.e.
// console.log('Step 1') -> close(() => console.log('Step 3')) -> console.log('Step 2')
// This should always output 'Step 1', 'Step 2', 'Step 3', but without the timeout it would output
// 'Step 1', 'Step 3', 'Step 2'.
setTimeout(cb)
}
}
} catch (e: any) {
logger.error(`Dialog closeCallback failed`, {
+3 -1
View File
@@ -295,6 +295,8 @@ function SaveButtonInner({
e.preventDefault()
e.stopPropagation()
const pinned = pin || false
try {
if (savedFeedConfig) {
await removeFeed(savedFeedConfig)
@@ -303,7 +305,7 @@ function SaveButtonInner({
{
type,
value: uri,
pinned: pin || false,
pinned,
},
])
}
+4 -2
View File
@@ -107,9 +107,10 @@ let PostControls = ({
return
}
const existingLike = post.viewer?.like
try {
setHasLikeIconBeenToggled(true)
if (!post.viewer?.like) {
if (!existingLike) {
sendInteraction({
item: post.uri,
event: 'app.bsky.feed.defs#interactionLike',
@@ -137,8 +138,9 @@ let PostControls = ({
return
}
const existingRepost = post.viewer?.repost
try {
if (!post.viewer?.repost) {
if (!existingRepost) {
sendInteraction({
item: post.uri,
event: 'app.bsky.feed.defs#interactionRepost',
+13 -5
View File
@@ -491,16 +491,21 @@ export function FollowButtonInner({
const onPressFollow = async (e: GestureResponderEvent) => {
e.preventDefault()
e.stopPropagation()
const displayNameOrHandle = profile.displayName || profile.handle
try {
await queueFollow()
Toast.show(
l`Following ${sanitizeDisplayName(
profile.displayName || profile.handle,
displayNameOrHandle,
moderation.ui('displayName'),
)}`,
)
onPressProp?.(e)
onFollow?.()
if (onPressProp) {
onPressProp(e)
}
if (onFollow) {
onFollow()
}
} catch (e) {
const err = e as Error
if (err?.name !== 'AbortError') {
@@ -514,15 +519,18 @@ export function FollowButtonInner({
const onPressUnfollow = async (e: GestureResponderEvent) => {
e.preventDefault()
e.stopPropagation()
const displayNameOrHandle = profile.displayName || profile.handle
try {
await queueUnfollow()
Toast.show(
l`No longer following ${sanitizeDisplayName(
profile.displayName || profile.handle,
displayNameOrHandle,
moderation.ui('displayName'),
)}`,
)
onPressProp?.(e)
if (onPressProp) {
onPressProp(e)
}
} catch (e) {
const err = e as Error
if (err?.name !== 'AbortError') {
@@ -221,6 +221,16 @@ function DialogInner({
const onPressSave = useCallback(async () => {
setImageError('')
setDisplayNameTooShort(false)
/*
* Hoisted above the `try`: React Compiler cannot lower a conditional
* expression inside one.
*/
const updatedMessage = isCurateList
? _(msg({message: 'User list updated', context: 'toast'}))
: _(msg({message: 'Moderation list updated', context: 'toast'}))
const createdMessage = isCurateList
? _(msg({message: 'User list created', context: 'toast'}))
: _(msg({message: 'Moderation list created', context: 'toast'}))
try {
if (displayName.length === 0) {
setDisplayNameTooShort(true)
@@ -244,11 +254,7 @@ function DialogInner({
descriptionFacets: richText.facets,
avatar: newListAvatar,
})
Toast.show(
isCurateList
? _(msg({message: 'User list updated', context: 'toast'}))
: _(msg({message: 'Moderation list updated', context: 'toast'})),
)
Toast.show(updatedMessage)
control.close(() => onSave?.(list.uri))
} else {
const {uri} = await createListMutation({
@@ -258,11 +264,7 @@ function DialogInner({
descriptionFacets: richText.facets,
avatar: newListAvatar,
})
Toast.show(
isCurateList
? _(msg({message: 'User list created', context: 'toast'}))
: _(msg({message: 'Moderation list created', context: 'toast'})),
)
Toast.show(createdMessage)
control.close(() => onSave?.(uri))
}
} catch (e: any) {
@@ -450,10 +450,16 @@ function DialogInner({
const feedRkey = useMemo(() => new AtUri(info.uri).rkey, [info.uri])
const onToggleLiked = async () => {
/*
* Hoisted out of the `try`: React Compiler cannot lower a logical
* expression in a test position there, and the `else` below rules out
* splitting this into nested ifs.
*/
const shouldUnlike = isLiked && likeUri
try {
playHaptic()
if (isLiked && likeUri) {
if (shouldUnlike) {
await unlikeFeed({uri: likeUri})
setLikeUri('')
ax.metric('feed:unlike', {feedUrl: info.uri})
@@ -95,11 +95,13 @@ function Inner({preferences}: {preferences: UsePreferencesQueryResponse}) {
const onSave = useCallback(async () => {
setError('')
const embeddingRules = maybeEditedPostgate.embeddingRules ?? []
try {
await setPostInteractionSettings({
threadgateAllowRules:
threadgateAllowUISettingToAllowRecordValue(maybeEditedAllowUI),
postgateEmbeddingRules: maybeEditedPostgate.embeddingRules ?? [],
postgateEmbeddingRules: embeddingRules,
})
Toast.show(_(msg({message: 'Settings saved', context: 'toast'})))
} catch (e: any) {
+16 -10
View File
@@ -78,11 +78,12 @@ export function StepFinished() {
logger.error('Failed to fetch starter pack', {safeMessage: e})
// don't tell the user, just get them through onboarding.
}
const starterPackList = starterPack?.list
try {
if (starterPack?.list) {
if (starterPackList) {
listItems = await getAllListMembers(
appviewClient,
starterPack.list.uri,
starterPackList.uri,
)
}
} catch (e) {
@@ -93,19 +94,24 @@ export function StepFinished() {
}
}
/*
* Hoisted above the `try`: React Compiler cannot lower these inside one, and
* `listItems` is already settled by the earlier try/catch.
*/
const followDids = [
BSKY_APP_ACCOUNT_DID,
...(listItems?.map(i => i.subject.did) ?? []),
]
const starterPackRef = starterPack
? {uri: starterPack.uri, cid: starterPack.cid}
: undefined
try {
const {interestsStepResults, profileStepResults} = state
const {selectedInterests} = interestsStepResults
await Promise.all([
bulkWriteFollows(
pdsClient,
appviewClient,
[BSKY_APP_ACCOUNT_DID, ...(listItems?.map(i => i.subject.did) ?? [])],
starterPack
? {uri: starterPack.uri, cid: starterPack.cid}
: undefined,
),
bulkWriteFollows(pdsClient, appviewClient, followDids, starterPackRef),
(async () => {
// Interests need to get saved first, then we can write the feeds to prefs
await pdsClient.call(setInterestsPref, {tags: selectedInterests})
@@ -259,6 +259,9 @@ export function HeaderLabelerButtons({
requireAuth(async (): Promise<void> => {
playHaptic()
const subscribe = !isSubscribed
const subscribeMetric = subscribe
? 'moderation:subscribedToLabeler'
: 'moderation:unsubscribedFromLabeler'
try {
await toggleSubscription({
@@ -266,12 +269,7 @@ export function HeaderLabelerButtons({
subscribe,
})
ax.metric(
subscribe
? 'moderation:subscribedToLabeler'
: 'moderation:unsubscribedFromLabeler',
{},
)
ax.metric(subscribeMetric, {})
} catch (e: any) {
reset()
if (e.message === 'MAX_LABELERS') {
@@ -238,14 +238,17 @@ export function HeaderStandardButtons({
const onPressFollow = () => {
playHaptic()
const displayNameOrHandle = profile.displayName || profile.handle
requireAuth(async () => {
try {
await queueFollow()
onFollow?.()
if (onFollow) {
onFollow()
}
Toast.show(
_(
msg`Following ${sanitizeDisplayName(
profile.displayName || profile.handle,
displayNameOrHandle,
moderation.ui('displayName'),
)}`,
),
@@ -264,14 +267,17 @@ export function HeaderStandardButtons({
const onPressUnfollow = () => {
playHaptic()
const displayNameOrHandle = profile.displayName || profile.handle
requireAuth(async () => {
try {
await queueUnfollow()
onUnfollow?.()
if (onUnfollow) {
onUnfollow()
}
Toast.show(
_(
msg`No longer following ${sanitizeDisplayName(
profile.displayName || profile.handle,
displayNameOrHandle,
moderation.ui('displayName'),
)}`,
),
@@ -64,6 +64,14 @@ export function Header({
const onTogglePinned = async () => {
playHaptic()
/*
* Hoisted above the `try`: inside it, `pinned` is `!savedFeedConfig.pinned`,
* which is `!isPinned` on the branch that uses this.
*/
const pinnedMessage = !isPinned
? _(msg`Pinned to your feeds`)
: _(msg`Unpinned from your feeds`)
try {
if (savedFeedConfig) {
const pinned = !savedFeedConfig.pinned
@@ -73,11 +81,7 @@ export function Header({
pinned,
},
])
Toast.show(
pinned
? _(msg`Pinned to your feeds`)
: _(msg`Unpinned from your feeds`),
)
Toast.show(pinnedMessage)
} else {
await addSavedFeeds([
{
@@ -5,6 +5,15 @@ import {type CaptchaWebViewProps} from './CaptchaWebView.shared'
const REDIRECT_HOST = new URL(window.location.href).host
/**
* Module scope because React Compiler cannot lower an optional chain inside a
* `try`, and this one has to stay in the `try` - reading `location` on a
* cross-origin frame throws.
*/
function getFrameHref(frame: HTMLIFrameElement | null): string | undefined {
return frame?.contentWindow?.location.href
}
export function CaptchaWebView({
url,
stateParam,
@@ -29,7 +38,7 @@ export function CaptchaWebView({
) as HTMLIFrameElement
try {
const href = frame?.contentWindow?.location.href
const href = getFrameHref(frame)
if (!href) return
const urlp = new URL(href)
@@ -37,7 +46,12 @@ export function CaptchaWebView({
if (urlp.host !== REDIRECT_HOST) return
const code = urlp.searchParams.get('code')
if (urlp.searchParams.get('state') !== stateParam || !code) {
const stateMismatch = urlp.searchParams.get('state') !== stateParam
if (stateMismatch) {
onError({error: 'Invalid state or code'})
return
}
if (!code) {
onError({error: 'Invalid state or code'})
return
}
+4 -1
View File
@@ -261,7 +261,10 @@ export function useGetJoinLinkPreview() {
staleTime: STALE.SECONDS.FIFTEEN,
})
const found = data.joinLinkPreviews[0]
return isKnownJoinLinkPreview(found) ? found : undefined
if (isKnownJoinLinkPreview(found)) {
return found
}
return undefined
} catch (error) {
logger.error('Failed to fetch join link preview', {safeMessage: error})
return undefined
+17 -6
View File
@@ -13,6 +13,22 @@ type ServiceConfig = {
}[]
}
/**
* Module scope because React Compiler cannot lower a `??` inside a `try`, and
* `data` only exists once the request in that `try` resolves.
*/
function toServiceConfig(data: {
checkEmailConfirmed?: boolean
liveNow?: ServiceConfig['liveNow']
}): ServiceConfig {
return {
checkEmailConfirmed: Boolean(data.checkEmailConfirmed),
// @ts-expect-error not included in the lexicon atm
topicsEnabled: Boolean(data.topicsEnabled),
liveNow: data.liveNow ?? [],
}
}
export function useServiceConfigQuery() {
const client = useAppviewClient()
return useQuery<ServiceConfig>({
@@ -22,12 +38,7 @@ export function useServiceConfigQuery() {
queryFn: async () => {
try {
const data = await client.call(app.bsky.unspecced.getConfig)
return {
checkEmailConfirmed: Boolean(data.checkEmailConfirmed),
// @ts-expect-error not included in the lexicon atm
topicsEnabled: Boolean(data.topicsEnabled),
liveNow: data.liveNow ?? [],
}
return toServiceConfig(data)
} catch (e) {
return {
checkEmailConfirmed: false,
+2 -1
View File
@@ -287,9 +287,10 @@ function DraftMediaPreview({post}: {post: DraftPostDisplay}) {
if (post.images && post.images.length > 0) {
const loaded: LoadedImage[] = []
for (const image of post.images) {
const alt = image.altText || ''
try {
const url = await storage.loadMediaFromLocal(image.localPath)
loaded.push({url, alt: image.altText || ''})
loaded.push({url, alt})
} catch (e) {
// Image doesn't exist locally, skip it
}
+14 -10
View File
@@ -1,4 +1,3 @@
import {useCallback} from 'react'
import * as MediaLibrary from 'expo-media-library/legacy'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
@@ -20,13 +19,23 @@ export function OpenCameraBtn({disabled, onAdd}: OpenCameraBtnProps) {
MediaLibrary.usePermissions({granularPermissions: ['photo']})
const t = useTheme()
const onPressTakePicture = useCallback(async () => {
const mediaGranted = mediaPermissionRes?.granted
const mediaCanAskAgain = mediaPermissionRes?.canAskAgain
/*
* No useCallback: with the diagnostics above resolved this component compiles,
* so React Compiler memoizes it, and the hand-written deps were what it could
* not preserve.
*/
const onPressTakePicture = async () => {
try {
if (!(await requestCameraAccessIfNeeded())) {
return
}
if (!mediaPermissionRes?.granted && mediaPermissionRes?.canAskAgain) {
await requestMediaPermission()
if (!mediaGranted) {
if (mediaCanAskAgain) {
await requestMediaPermission()
}
}
const img = await openCamera({
@@ -49,12 +58,7 @@ export function OpenCameraBtn({disabled, onAdd}: OpenCameraBtnProps) {
// ignore
logger.warn('Error using camera', {error: err})
}
}, [
onAdd,
requestCameraAccessIfNeeded,
mediaPermissionRes,
requestMediaPermission,
])
}
const shouldShowCameraButton = IS_NATIVE || IS_WEB_MOBILE
if (!shouldShowCameraButton) {
+17 -5
View File
@@ -56,8 +56,10 @@ export function ComposerPrompt() {
requestVideoAccessIfNeeded(),
])
if (!photoAccess && !videoAccess) {
return
if (!photoAccess) {
if (!videoAccess) {
return
}
}
if (Keyboard.isVisible()) {
@@ -108,8 +110,10 @@ export function ComposerPrompt() {
return
}
if (IS_NATIVE && Keyboard.isVisible()) {
Keyboard.dismiss()
if (IS_NATIVE) {
if (Keyboard.isVisible()) {
Keyboard.dismiss()
}
}
const image = await openCamera({
@@ -127,8 +131,16 @@ export function ComposerPrompt() {
},
]
/*
* Statement form rather than a ternary: React Compiler cannot lower a
* conditional expression inside a `try`, and `imageUris` is built here.
*/
let nativeImageUris
if (IS_NATIVE) {
nativeImageUris = imageUris
}
openComposer({
imageUris: IS_NATIVE ? imageUris : undefined,
imageUris: nativeImageUris,
logContext: 'Fab',
})
} catch (err: any) {
+16 -9
View File
@@ -79,15 +79,22 @@ export function UserBanner({
try {
if (IS_NATIVE) {
onSelectNewBanner?.(
await compressIfNeeded(
await openCropper({
imageUri: items[0].path,
aspectRatio: 3 / 1,
}),
IMAGE_SIZE_CONFIG_2K_1MB,
),
)
/*
* Nested rather than `?.()`: React Compiler cannot lower an optional
* call inside a `try`. Like `?.()`, this leaves the arguments
* unevaluated when the callback is absent.
*/
if (onSelectNewBanner) {
onSelectNewBanner(
await compressIfNeeded(
await openCropper({
imageUri: items[0].path,
aspectRatio: 3 / 1,
}),
IMAGE_SIZE_CONFIG_2K_1MB,
),
)
}
} else {
setRawImage(await createComposerImage(items[0]))
editImageDialogControl.open()