Unblock React Compiler for 18 components with value blocks inside try

React Compiler cannot lower a conditional expression - `&&`, `||`, `??`, `?.`,
a ternary - inside a try block. Three techniques, picked per site:

- split `if (a && b)` into nested ifs, where there is no `else` to break
- hoist the expression into a const above the try, where it does not depend on
  anything the try produces
- move it into a module-scope helper, where it does

Optional calls become `if (f) f()`, which keeps the arguments unevaluated when
the callback is absent, exactly as `f?.()` does.

Skipped components: 125 -> 107.

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