diff --git a/.jscodeshift/react-import.js b/.jscodeshift/react-import.js new file mode 100644 index 0000000000..e63238f862 --- /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 index 18f112f0b6..21cdd850a5 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -37,6 +37,7 @@ export default defineConfig( '*.e2e.ts', '*.e2e.tsx', 'eslint.config.mjs', + '.jscodeshift/**', ], }, diff --git a/src/App.native.tsx b/src/App.native.tsx index aa6e9bd06b..3699cb173d 100644 --- 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 @@ function InnerApp() { - @@ -208,7 +208,7 @@ function InnerApp() { - + @@ -220,7 +220,7 @@ function InnerApp() { 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 index 5d46ba617e..b453cb4292 100644 --- 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 @@ export function Splash(props: React.PropsWithChildren) { 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 index 3aff9ddb57..1c7b080d87 100644 --- 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 @@ export type Alf = { /* * 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 @@ export function ThemeProvider({ }, [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 @@ export function ThemeProvider({ [setFontFamily], ) - const value = React.useMemo( + const value = useMemo( () => ({ themes, themeName: themeName, @@ -122,12 +118,12 @@ export function ThemeProvider({ } 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 index 836aac3073..7cb9b12723 100644 --- 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 @@ import {IS_WEB} from '#/env' 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 index 265d0f23c2..f7dcef51bd 100644 --- 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 @@ export function useGutters([top, right, bottom, left]: Gutter[]) { 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 index 4a43bf283c..a65eeb0722 100644 --- 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 @@ export function AccountList({ t.atoms.border_contrast_low, ]}> {accounts.map(account => ( - + p.did === account.did)} account={account} @@ -61,7 +61,7 @@ export function AccountList({ isPendingAccount={account.did === pendingDid} /> - + ))}