diff --git a/.jscodeshift/react-import.js b/.jscodeshift/react-import.js new file mode 100644 --- /dev/null +++ b/.jscodeshift/react-import.js @@ -0,0 +1,135 @@ +/** + * Codemod to replace namespaced React calls with named imports + * + * Before: + * import React from 'react' + * React.useEffect(() => {}, []) + * + * After: + * import { useEffect } from 'react' + * useEffect(() => {}, []) + * + * Usage: jscodeshift -t .jscodeshift/react-import.js + * Example: jscodeshift -t .jscodeshift/react-import.js src/App.native.tsx + */ + +/* eslint-disable */ + +export const parser = 'tsx' + +export default function transformer(file, api) { + const j = api.jscodeshift + const root = j(file.source) + + // Find the React import + let reactImportPath = null + const reactMembers = new Set() + + root.find(j.ImportDeclaration).forEach(path => { + const node = path.value + if (node.source.value === 'react') { + node.specifiers.forEach(spec => { + // Check if this is a default import of React + if ( + spec.type === 'ImportDefaultSpecifier' && + spec.local.name === 'React' + ) { + reactImportPath = path + } + }) + } + }) + + if (!reactImportPath) { + // No React import found, nothing to do + return file.source + } + + // Find all React.* member expressions + root + .find(j.MemberExpression) + .filter(path => { + const node = path.value + return ( + node.object.type === 'Identifier' && + node.object.name === 'React' && + node.property.type === 'Identifier' + ) + }) + .forEach(path => { + const propertyName = path.value.property.name + reactMembers.add(propertyName) + }) + + // Find all React.* JSX member expressions (e.g., ) + root + .find(j.JSXMemberExpression) + .filter(path => { + const node = path.value + return node.object.name === 'React' && node.property.name + }) + .forEach(path => { + const propertyName = path.value.property.name + reactMembers.add(propertyName) + }) + + // If no React members are used, remove the import + if (reactMembers.size === 0) { + reactImportPath.prune() + return root.toSource() + } + + // Sort the members for consistent output + const sortedMembers = Array.from(reactMembers).sort() + + // Create new import specifiers + const newSpecifiers = sortedMembers.map(name => + j.importSpecifier(j.identifier(name), j.identifier(name)), + ) + + // Get the existing import specifiers + const sortedImports = Array.from(reactImportPath.value.specifiers).sort() + const existingSpecifiers = sortedImports.filter( + specifier => specifier.type !== 'ImportDefaultSpecifier', + ) + + const allSpecifiers = [ + ...new Map( + [...existingSpecifiers, ...newSpecifiers].map(item => [ + item.imported.name, + item, + ]), + ).values(), + ] + + // Update the import declaration + reactImportPath.value.specifiers = allSpecifiers + + // Replace all React.* member expressions with just the identifier + root + .find(j.MemberExpression) + .filter(path => { + const node = path.value + return ( + node.object.type === 'Identifier' && + node.object.name === 'React' && + node.property.type === 'Identifier' + ) + }) + .replaceWith(path => { + return j.identifier(path.value.property.name) + }) + + // Replace all React.* JSX member expressions with just the identifier + root + .find(j.JSXMemberExpression) + .filter(path => { + const node = path.value + return node.object.name === 'React' && node.property.name + }) + .replaceWith(path => { + return j.jsxIdentifier(path.value.property.name) + }) + + return root.toSource() +} diff --git a/eslint.config.mjs b/eslint.config.mjs --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -37,6 +37,7 @@ 'src/locale/locales/**/*.js', '*.e2e.ts', '*.e2e.tsx', 'eslint.config.mjs', + '.jscodeshift/**', ], }, diff --git a/src/App.native.tsx b/src/App.native.tsx --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -1,7 +1,7 @@ import '#/logger/sentry/setup' import '#/view/icons' -import React, {useEffect, useState} from 'react' +import {Fragment, useEffect, useState} from 'react' import {GestureHandlerRootView} from 'react-native-gesture-handler' import {KeyboardProvider as KeyboardControllerProvider} from 'react-native-keyboard-controller' import { @@ -111,7 +111,7 @@ prefetchLiveEvents() prefetchAppConfig() function InnerApp() { - const [isReady, setIsReady] = React.useState(false) + const [isReady, setIsReady] = useState(false) const {currentAccount} = useSession() const {resumeSession} = useSessionApi() const theme = useColorModeTheme() @@ -152,7 +152,7 @@ - @@ -208,7 +208,7 @@ - + @@ -220,7 +220,7 @@ function App() { const [isReady, setReady] = useState(false) - React.useEffect(() => { + useEffect(() => { Promise.all([initPersistedState(), Geo.resolve(), setupDeviceId]).then(() => setReady(true), ) diff --git a/src/Splash.tsx b/src/Splash.tsx --- a/src/Splash.tsx +++ b/src/Splash.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useEffect} from 'react' +import {forwardRef, useCallback, useEffect, useState} from 'react' import { AccessibilityInfo, Image as RNImage, @@ -29,7 +29,7 @@ const darkSplashImageUri = RNImage.resolveAssetSource( darkSplashImagePointer, ).uri -export const Logo = React.forwardRef(function LogoImpl(props: SvgProps, ref) { +export const Logo = forwardRef(function LogoImpl(props: SvgProps, ref) { const width = 1000 const height = width * (67 / 64) return ( @@ -58,12 +58,10 @@ const intro = useSharedValue(0) const outroLogo = useSharedValue(0) const outroApp = useSharedValue(0) const outroAppOpacity = useSharedValue(0) - const [isAnimationComplete, setIsAnimationComplete] = React.useState(false) - const [isImageLoaded, setIsImageLoaded] = React.useState(false) - const [isLayoutReady, setIsLayoutReady] = React.useState(false) - const [reduceMotion, setReduceMotion] = React.useState( - false, - ) + const [isAnimationComplete, setIsAnimationComplete] = useState(false) + const [isImageLoaded, setIsImageLoaded] = useState(false) + const [isLayoutReady, setIsLayoutReady] = useState(false) + const [reduceMotion, setReduceMotion] = useState(false) const isReady = props.isReady && isImageLoaded && diff --git a/src/alf/index.tsx b/src/alf/index.tsx --- a/src/alf/index.tsx +++ b/src/alf/index.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import {createContext, useCallback, useContext, useMemo, useState} from 'react' import {type Theme, type ThemeName} from '@bsky.app/alf' import { @@ -46,7 +46,7 @@ /* * Context */ -export const Context = React.createContext({ +export const Context = createContext({ themeName: 'light', theme: themes.light, themes, @@ -65,15 +65,13 @@ export function ThemeProvider({ children, theme: themeName, }: React.PropsWithChildren<{theme: ThemeName}>) { - const [fontScale, setFontScale] = React.useState(() => + const [fontScale, setFontScale] = useState(() => getFontScale(), ) - const [fontScaleMultiplier, setFontScaleMultiplier] = React.useState(() => + const [fontScaleMultiplier, setFontScaleMultiplier] = useState(() => computeFontScaleMultiplier(fontScale), ) - const setFontScaleAndPersist = React.useCallback< - Alf['fonts']['setFontScale'] - >( + const setFontScaleAndPersist = useCallback( fs => { setFontScale(fs) persistFontScale(fs) @@ -81,12 +79,10 @@ setFontScaleMultiplier(computeFontScaleMultiplier(fs)) }, [setFontScale], ) - const [fontFamily, setFontFamily] = React.useState( - () => getFontFamily(), + const [fontFamily, setFontFamily] = useState(() => + getFontFamily(), ) - const setFontFamilyAndPersist = React.useCallback< - Alf['fonts']['setFontFamily'] - >( + const setFontFamilyAndPersist = useCallback( ff => { setFontFamily(ff) persistFontFamily(ff) @@ -94,7 +90,7 @@ }, [setFontFamily], ) - const value = React.useMemo( + const value = useMemo( () => ({ themes, themeName: themeName, @@ -122,12 +118,12 @@ return {children} } export function useAlf() { - return React.useContext(Context) + return useContext(Context) } export function useTheme(theme?: ThemeName) { const alf = useAlf() - return React.useMemo(() => { + return useMemo(() => { return theme ? alf.themes[theme] : alf.theme }, [theme, alf]) } diff --git a/src/alf/util/useColorModeTheme.ts b/src/alf/util/useColorModeTheme.ts --- a/src/alf/util/useColorModeTheme.ts +++ b/src/alf/util/useColorModeTheme.ts @@ -1,4 +1,4 @@ -import React from 'react' +import {useLayoutEffect} from 'react' import {type ColorSchemeName, useColorScheme} from 'react-native' import {type ThemeName} from '@bsky.app/alf' @@ -9,7 +9,7 @@ export function useColorModeTheme(): ThemeName { const theme = useThemeName() - React.useLayoutEffect(() => { + useLayoutEffect(() => { updateDocument(theme) }, [theme]) diff --git a/src/alf/util/useGutters.ts b/src/alf/util/useGutters.ts --- a/src/alf/util/useGutters.ts +++ b/src/alf/util/useGutters.ts @@ -1,4 +1,4 @@ -import React from 'react' +import {useMemo} from 'react' import {type Breakpoint, useBreakpoints} from '#/alf/breakpoints' import * as tokens from '#/alf/tokens' @@ -52,7 +52,7 @@ } else if (bottom === undefined) { bottom = top left = right } - return React.useMemo(() => { + return useMemo(() => { return { paddingTop: top === 0 ? 0 : gutters[top][activeBreakpoint || 'default'], paddingRight: diff --git a/src/components/AccountList.tsx b/src/components/AccountList.tsx --- a/src/components/AccountList.tsx +++ b/src/components/AccountList.tsx @@ -1,4 +1,4 @@ -import React, {useCallback} from 'react' +import {Fragment, useCallback} from 'react' import {View} from 'react-native' import {type AppBskyActorDefs} from '@atproto/api' import {msg} from '@lingui/core/macro' @@ -52,7 +52,7 @@ a.border, t.atoms.border_contrast_low, ]}> {accounts.map(account => ( - + p.did === account.did)} account={account} @@ -61,7 +61,7 @@ isCurrentAccount={account.did === currentAccount?.did} isPendingAccount={account.did === pendingDid} /> - + ))} ) } -DrawerFooter = React.memo(DrawerFooter) +DrawerFooter = memo(DrawerFooter) interface MenuItemProps extends ComponentProps { icon: JSX.Element @@ -408,7 +405,7 @@ onPress={onPress} /> ) } -SearchMenuItem = React.memo(SearchMenuItem) +SearchMenuItem = memo(SearchMenuItem) let HomeMenuItem = ({ isActive, @@ -434,7 +431,7 @@ onPress={onPress} /> ) } -HomeMenuItem = React.memo(HomeMenuItem) +HomeMenuItem = memo(HomeMenuItem) let ChatMenuItem = ({ isActive, @@ -460,7 +457,7 @@ onPress={onPress} /> ) } -ChatMenuItem = React.memo(ChatMenuItem) +ChatMenuItem = memo(ChatMenuItem) let NotificationsMenuItem = ({ isActive, @@ -498,7 +495,7 @@ onPress={onPress} /> ) } -NotificationsMenuItem = React.memo(NotificationsMenuItem) +NotificationsMenuItem = memo(NotificationsMenuItem) let FeedsMenuItem = ({ isActive, @@ -524,7 +521,7 @@ onPress={onPress} /> ) } -FeedsMenuItem = React.memo(FeedsMenuItem) +FeedsMenuItem = memo(FeedsMenuItem) let ListsMenuItem = ({onPress}: {onPress: () => void}): React.ReactNode => { const {_} = useLingui() @@ -538,7 +535,7 @@ onPress={onPress} /> ) } -ListsMenuItem = React.memo(ListsMenuItem) +ListsMenuItem = memo(ListsMenuItem) let BookmarksMenuItem = ({ isActive, @@ -564,7 +561,7 @@ onPress={onPress} /> ) } -BookmarksMenuItem = React.memo(BookmarksMenuItem) +BookmarksMenuItem = memo(BookmarksMenuItem) let ProfileMenuItem = ({ isActive, @@ -589,7 +586,7 @@ onPress={onPress} /> ) } -ProfileMenuItem = React.memo(ProfileMenuItem) +ProfileMenuItem = memo(ProfileMenuItem) let SettingsMenuItem = ({onPress}: {onPress: () => void}): React.ReactNode => { const {_} = useLingui() @@ -602,7 +599,7 @@ onPress={onPress} /> ) } -SettingsMenuItem = React.memo(SettingsMenuItem) +SettingsMenuItem = memo(SettingsMenuItem) function MenuItem({icon, label, count, bold, onPress}: MenuItemProps) { const t = useTheme() diff --git a/src/view/shell/NavSignupCard.tsx b/src/view/shell/NavSignupCard.tsx --- a/src/view/shell/NavSignupCard.tsx +++ b/src/view/shell/NavSignupCard.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import {memo, useCallback} from 'react' import {View} from 'react-native' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' @@ -18,12 +18,12 @@ const {_} = useLingui() const {requestSwitchToAccount} = useLoggedOutViewControls() const closeAllActiveElements = useCloseAllActiveElements() - const showSignIn = React.useCallback(() => { + const showSignIn = useCallback(() => { closeAllActiveElements() requestSwitchToAccount({requestedAccount: 'none'}) }, [requestSwitchToAccount, closeAllActiveElements]) - const showCreateAccount = React.useCallback(() => { + const showCreateAccount = useCallback(() => { closeAllActiveElements() requestSwitchToAccount({requestedAccount: 'new'}) // setShowLoggedOut(true) @@ -71,5 +71,5 @@ ) } -NavSignupCard = React.memo(NavSignupCard) +NavSignupCard = memo(NavSignupCard) export {NavSignupCard} diff --git a/src/view/shell/bottom-bar/BottomBarWeb.tsx b/src/view/shell/bottom-bar/BottomBarWeb.tsx --- a/src/view/shell/bottom-bar/BottomBarWeb.tsx +++ b/src/view/shell/bottom-bar/BottomBarWeb.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import {useCallback} from 'react' import {View} from 'react-native' import Animated from 'react-native-reanimated' import {msg, plural} from '@lingui/core/macro' @@ -61,18 +61,18 @@ const unreadMessageCount = useUnreadMessageCount() const notificationCountStr = useUnreadNotifications() - const showSignIn = React.useCallback(() => { + const showSignIn = useCallback(() => { closeAllActiveElements() requestSwitchToAccount({requestedAccount: 'none'}) }, [requestSwitchToAccount, closeAllActiveElements]) - const showCreateAccount = React.useCallback(() => { + const showCreateAccount = useCallback(() => { closeAllActiveElements() requestSwitchToAccount({requestedAccount: 'new'}) // setShowLoggedOut(true) }, [requestSwitchToAccount, closeAllActiveElements]) - const onLongPressProfile = React.useCallback(() => { + const onLongPressProfile = useCallback(() => { accountSwitchControl.open() }, [accountSwitchControl])