mcpField
U
unistyles-v2-to-v3-migration

unistyles-v2-to-v3-migration

github.com/jpudysz开发3.0k75 次安装今天更新
代码仓库

关于 unistyles-v2-to-v3-migration

这是一份将 React Native 项目从 react-native-unistyles v2 迁移到 v3 的技能,面向负责升级的开发者。它按步骤涵盖构建配置、API 替换、运行时调整和测试。

主要功能

  1. 01按顺序列出 v2 到 v3 迁移步骤
  2. 02提供 Babel 与 Metro 配置说明
  3. 03说明 API 替换及运行时变化
  4. 04涵盖 Expo Router 集成
  5. 05说明 TypeScript 与测试配置

适用场景

  1. 01规划 Unistyles v3 升级
  2. 02更新样式与主题访问方式
  3. 03调整 Expo Router 应用
  4. 04迁移后配置测试

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/jpudysz/react-native-unistyles --skill unistyles-v2-to-v3-migration

安装后在 Claude Code 中输入/unistyles-v2-to-v3-migration确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
common-pitfalls.md
migration-patterns.md
v3-api-reference.md

详细介绍

Unistyles v2 到 v3 迁移技能

你正在将 React Native 代码库从 react-native-unistyles v2 迁移到 v3。请严格按照此工作流操作。v3 经全面重写,采用 C++ 核心(Nitro Modules),不会触发重新渲染,并通过 Babel 插件在构建时处理 StyleSheets。

前提条件

  • React Native 0.81.0+,必须启用新架构(自 RN 0.76 起默认启用,自 RN 0.82 起是唯一架构)
  • React 19+(Unistyles 会在运行时强制要求)
  • react-native-nitro-modules(原生桥接依赖;Unistyles 3.4.0+ 要求 0.37.1+)
  • 不需要 react-native-edge-to-edge(自 v3.1.0 起为可选,现已弃用)——请改为在 android/gradle.properties 中设置 edgeToEdgeEnabled=true(Expo SDK 54+ 会自动启用)
  • Expo SDK 54+(如果使用 Expo;与 Expo Go 不兼容——需要开发客户端或预构建)
  • Xcode 16.4+(iOS,Nitro Modules 的要求),iOS 15.1+

迁移工作流

请按顺序执行以下步骤。完成每个步骤后才能继续下一步。

第 1 步:安装 v3 并配置 Babel 插件

安装 v3 和 Nitro Modules,然后重新构建原生应用(pods / Gradle):

yarn add react-native-unistyles@3 [email protected]  # or npm / bun / pnpm

添加 Babel 插件:

// babel.config.js
module.exports = {
  plugins: [
    ['react-native-unistyles/plugin', { root: 'src' }]  // your app source root
  ]
}

root 选项为必填项(缺少该选项时插件会报错,且该选项不能指向项目根目录本身)。root 下的每个文件都会被处理,无论它导入了什么。root 之外的文件只有在导入 react-native-unistyles、包含 autoProcessImports 中列出的导入项,或匹配 autoProcessPaths(其中始终包含两个 react-native-reanimated 组件路径;你添加的路径会追加到其中)时才会被处理。除非通过 autoProcessPaths / autoRemapImports 列入白名单,否则会忽略 node_modules。

如果使用 React Compiler,Unistyles 插件必须放在 plugins 数组中 React Compiler 之前。

自 3.6.0 起,Metro 应用也可以改用实验性的零配置 Metro 集成:使用来自 react-native-unistyles/metro 的 withUnistylesConfig,并配置 module.exports = withUnistylesConfig(getDefaultConfig(__dirname)),同时不要在 babel.config.js 中添加 Unistyles 条目(两者同时存在会报错)。这种方式不需要 root / autoProcess* 选项,并会处理 node_modules。对于在 babel.config.js 的 plugins 中列出 babel-plugin-react-compiler 的裸 RN 应用,请继续使用 Babel 插件。Babel 插件仍是默认选项。

如果应用使用 Re.Pack 打包,请不要在 babel.config.js 中添加此插件——应改用来自 react-native-unistyles/repack-plugin 的 RepackUnistylePlugin,并设置 unistylesPluginOptions: { root: 'src' }。

第 2 步:将 UnistylesRegistry 替换为 StyleSheet.configure
- import { UnistylesRegistry } from 'react-native-unistyles'
+ import { StyleSheet } from 'react-native-unistyles'

- UnistylesRegistry
-   .addThemes({ light: lightTheme, dark: darkTheme })
-   .addBreakpoints({ sm: 0, md: 768, lg: 1200 })
-   .addConfig({
-     adaptiveThemes: true,
-     initialTheme: 'dark',
-     plugins: [myPlugin],
-     experimentalCSSMediaQueries: true,
-     windowResizeDebounceTimeMs: 100,
-     disableAnimatedInsets: true
-   })
+ StyleSheet.configure({
+   themes: { light: lightTheme, dark: darkTheme },
+   breakpoints: { sm: 0, md: 768, lg: 1200 },
+   settings: {
+     adaptiveThemes: true // OR initialTheme: 'dark' - never both
+   }
+ })

已移除的设置: plugins、experimentalCSSMediaQueries(现已始终启用)、windowResizeDebounceTimeMs(不再进行防抖)、disableAnimatedInsets(insets 不再触发重新渲染)。

v3 的 settings 仅接受以下选项: adaptiveThemes、initialTheme、CSSVars(web)、nativeBreakpointsMode(默认值为 'pixels' | 'points')。任何其他键都会报错。initialTheme 与 adaptiveThemes: true 不能同时使用(互斥);adaptiveThemes 要求同时配置 light 和 dark 主题;第一个断点必须为 0。必须在求值任何 StyleSheet.create 之前调用 StyleSheet.configure(在入口文件中先导入配置文件)。

第 3 步:替换所有 StyleSheet 导入并使用 createStyleSheet

Unistyles 的 StyleSheet 是 React Native StyleSheet 的完整 polyfill——包含 hairlineWidth、compose、flatten、absoluteFill 和 absoluteFillObject。应将 所有 import { StyleSheet } from 'react-native' 替换为 import { StyleSheet } from 'react-native-unistyles',以便统一导入来源。

- import { StyleSheet } from 'react-native'
- import { createStyleSheet } from 'react-native-unistyles'
+ import { StyleSheet } from 'react-native-unistyles'

- const stylesheet = createStyleSheet(theme => ({
+ const styles = StyleSheet.create(theme => ({
    container: {
      backgroundColor: theme.colors.background
    }
  }))
第 4 步:移除所有 useStyles 钩子
- import { useStyles } from 'react-native-unistyles'

  const MyComponent = () => {
-   const { styles, theme } = useStyles(stylesheet)
    return <View style={styles.container} />
  }

使用 StyleSheet.create 创建的样式可直接使用,无需钩子。Babel 插件会在构建时处理响应式更新。

动态函数(styles.box(width, color))仍可正常使用,但其参数必须可序列化(字符串、数字、布尔值、null、undefined、普通数组/对象),因为 Unistyles 会将它们存储在 C++ 中,并在主题/运行时发生变化时重新运行函数。函数会被移除,Date / Map / Set / 类实例会变成普通对象——请改为传入预先计算的值。

第 5 步:将 useInitialTheme 替换为 settings.initialTheme
- import { useInitialTheme } from 'react-native-unistyles'
-
- const App = () => {
-   useInitialTheme(storage.getString('preferredTheme') ?? 'light')
-   return <Stack />
- }

+ // In your configure call:
+ StyleSheet.configure({
+   settings: {
+     initialTheme: () => storage.getString('preferredTheme') ?? 'light'
+   }
+ })

initialTheme 接受字符串或同步函数。

第 6 步:替换用于访问主题的 useStyles()

对于仅使用 useStyles()(不带样式表)来获取主题/运行时的组件:

选项 A - 使用 withUnistyles(推荐用于传递由主题派生的属性):

import { withUnistyles } from 'react-native-unistyles'

const UniButton = withUnistyles(Button, (theme, rt) => ({
  color: theme.colors.primary,
  size: rt.screen.width > 400 ? 'large' : 'small'
}))

// Usage: <UniButton />

选项 B - 使用 useUnistyles 钩子(快速迁移方式):

import { useUnistyles } from 'react-native-unistyles'

const MyComponent = () => {
  const { theme, rt } = useUnistyles()
  return <Text style={{ color: theme.colors.primary }}>{rt.screen.width}</Text>
}

警告: 每当你读取的 theme / rt 值发生变化时,useUnistyles 都会重新渲染整个组件(订阅是在访问属性时创建的,而非解构时)。为获得更好的性能,请优先使用 withUnistyles 或 StyleSheet.create(theme => ...)。请注意,在 iOS/Android 上,withUnistyles 组件在主题变化时始终会重新渲染。withUnistyles 映射接收的是精简运行时,不能返回 style / contentContainerStyle(这些会自动映射),并且可以返回 key 以强制重新挂载。

第 7 步:更新变体选择
- const { styles } = useStyles(stylesheet, { size: 'large', color: 'primary' })
+ styles.useVariants({ size: 'large', color: 'primary' })

在组件顶部调用 styles.useVariants()(类似钩子)。必须先调用它,再访问使用变体的样式。它始终要求传入一个对象——传入 {} 可选择 default 变体;在 iOS/Android 上调用 styles.useVariants(undefined) 会抛出错误。如果不调用 useVariants,变体(包括 default)都会被忽略。

第 8 步:修复样式展开(至关重要)

v3 样式是 C++ 代理对象。展开会破坏绑定。

- <View style={{ ...styles.container, ...styles.extra }} />
+ <View style={[styles.container, styles.extra]} />

- <View style={{ ...styles.container, marginTop: 10 }} />
+ <View style={[styles.container, { marginTop: 10 }]} />

绝不要使用 {...styles.x}。始终使用 [styles.x, styles.y] 数组语法。

第 9 步:移除插件(改用主题中的静态函数)

插件系统已移除。请在主题或 StyleSheet 中使用静态函数替代插件:

- // Plugin approach (v2)
- const fontPlugin: UnistylesPlugin = {
-   name: 'fontPlugin',
-   onParsedStyle: (_key, styles) => {
-     if ('fontWeight' in styles) {
-       styles.fontFamily = styles.fontWeight === 'bold' ? 'Roboto-Bold' : 'Roboto-Regular'
-     }
-     return styles
-   }
- }

+ // v3: Use a helper function in your theme or directly
+ const styles = StyleSheet.create(theme => ({
+   text: {
+     fontFamily: theme.utils.getFontFamily('bold')
+   }
+ }))
第 10 步:移除 UnistylesProvider

UnistylesProvider 已不存在。只需将其从组件树中移除。

第 11 步:更新 UnistylesRuntime 的用法

已重命名/更改的方法(其余方法请参照 TypeScript 类型):

  • UnistylesRuntime.setRootViewBackgroundColor(color?) - 不再有 alpha 参数;接受任何 React Native 颜色,省略参数可重置为透明
  • 使用 StyleSheet.hairlineWidth 替代 UnistylesRuntime.hairlineWidth

已移除的方法:

  • addPlugin(plugin)、removePlugin(plugin)、enabledPlugins
  • statusBar.setColor(color)、navigationBar.setColor(color)(Android 15 中已弃用)

UnistylesRuntime 中的 v3 系统栏/内边距 API:

  • statusBar.setHidden(hidden, animation?) - animation 为 'none' | 'fade' | 'slide'(用于 iOS)
  • statusBar.setStyle(style, animated?) - style 是 StatusBarStyle 枚举(StatusBarStyle.Default | Light | Dark,从 react-native-unistyles 导出)
  • navigationBar.setHidden(hidden) - 仅适用于 Android(iOS 上导航栏尺寸始终为 0)
  • setImmersiveMode(enabled) - 在 Android 上隐藏状态栏和导航栏,在 iOS 上仅隐藏状态栏
  • 键盘内边距使用 insets.ime

只能通过 UnistylesRuntime.statusBar / UnistylesRuntime.navigationBar 访问系统栏——没有独立的状态栏/导航栏导出项。

第 12 步:更新 TypeScript 声明
- type AppThemes = { light: typeof lightTheme, dark: typeof darkTheme }
+ type AppThemes = typeof themes  // where themes = { light: lightTheme, dark: darkTheme }
+ type AppBreakpoints = typeof breakpoints

  declare module 'react-native-unistyles' {
    export interface UnistylesThemes extends AppThemes {}
+   export interface UnistylesBreakpoints extends AppBreakpoints {}
  }

interface X extends typeof y 不是有效的 TypeScript 语法——务必通过类型别名实现。v3 中实用的导出项:枚举 ColorScheme、Orientation、StatusBarStyle、IOSContentSizeCategory、AndroidContentSizeCategory、WebContentSizeCategory,以及类型 UnistylesVariants、UnistylesMiniRuntime。

步骤 13:更新键盘/IME 处理
- import { useAnimatedKeyboard } from 'react-native-reanimated'
- const keyboard = useAnimatedKeyboard()

+ // Use ime inset in StyleSheet
+ const styles = StyleSheet.create((theme, rt) => ({
+   container: {
+     paddingBottom: rt.insets.ime
+   }
+ }))
步骤 14:设置测试模拟
// package.json (or jest.config.js)
{
  "jest": {
    "setupFiles": [
      "react-native-unistyles/mocks",
      "./unistyles.ts"
    ]
  }
}

配置文件(即你的 StyleSheet.configure 调用)必须放在模拟项之后。NODE_ENV=test 时,Babel 插件不执行任何操作(Jest 的默认值)。模拟项始终使用第一个注册的主题,将运行时尺寸/内边距报告为 0,移除变体,并且不渲染 Display / Hide / ScopedTheme——不要断言解析后的样式。

Expo Router 集成

如果项目使用 Expo Router,则需要额外的步骤,因为 Expo Router 会在 Unistyles 初始化前解析路由。

1. 更改 package.json 中的主入口:

{ "main": "index.ts" }

2. 创建 index.ts,在路由器之前导入 Unistyles 配置:

import 'expo-router/entry'
import './unistyles' // your StyleSheet.configure() file

3. 对于静态渲染(Expo SDK 52+): 也在 +html.tsx 中导入 Unistyles 配置:

import '../unistyles' // ensures Unistyles initializes for each static page

import { ScrollViewStyleReset } from 'expo-router/html';
import { type PropsWithChildren } from 'react';

// This file is web-only and used to configure the root HTML for every
// web page during static rendering.
// The contents of this function only run in Node.js environments and
// do not have access to the DOM or browser APIs.
export default function Root({ children }: PropsWithChildren) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta httpEquiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />

        {/*
          Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
          However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
        */}
        <ScrollViewStyleReset />

        {/* Add any additional <head> elements that you want globally available on web... */}
      </head>
      <body>{children}</body>
    </html>
  );
}

完整详情请参阅 https://www.unistyl.es/v3/guides/expo-router。

快速参考:v2 API 到 v3

v2v3
createStyleSheetStyleSheet.create
useStyles(stylesheet)直接使用 styles(无需 Hook)
用 useStyles() 获取主题useUnistyles() 或 withUnistyles
useStyles(ss, variants)styles.useVariants(variants)
useInitialTheme(name)configure 中的 settings.initialTheme
UnistylesRegistry.addThemes()StyleSheet.configure({ themes })
UnistylesRegistry.addBreakpoints()StyleSheet.configure({ breakpoints })
UnistylesRegistry.addConfig()StyleSheet.configure({ settings })
UnistylesProvider已移除(无需使用)
UnistylesPlugin已移除(改用主题函数)
{...styles.x, ...styles.y}[styles.x, styles.y]
UnistylesRuntime.hairlineWidthStyleSheet.hairlineWidth
statusBar.setColor()已移除
navigationBar.setColor()已移除
import { StyleSheet } from 'react-native'import { StyleSheet } from 'react-native-unistyles'(完整 polyfill)

决策树:第三方组件

当第三方组件需要主题值或 Unistyles 样式时:

  1. 它是带有 style 属性的 react-native / react-native-reanimated 组件吗? -> 直接传入 Unistyles 样式,无需其他操作
  2. 它是带有 contentContainerStyle 的 react-native 组件吗? -> 使用 withUnistyles 包装(自动映射 style 和 contentContainerStyle)
  3. 它是内部使用 react-native 组件的第三方组件吗? -> 在 Babel 插件中通过 autoProcessPaths(非标准导入则使用 autoRemapImports)将其加入白名单
  4. 它需要从主题派生的非样式属性(例如 color),或者步骤 3 未能解决问题吗? -> 使用 withUnistyles 包装
  5. 以上方法都不奏效吗? -> 使用 useUnistyles() Hook 作为后备方案

关键规则

  1. 必须使用新架构——自 RN 0.76 起默认启用,自 RN 0.82 起是唯一架构;在 RN 0.81 上,请确保未选择退出
  2. 绝不要展开样式——始终使用数组语法 [styles.a, styles.b]
  3. 必须进行构建时集成——可使用 Babel 插件(默认)、实验性的 Metro 集成(3.6.0+,绝不能与 Babel 插件同时使用),或用于 Re.Pack 的 RepackUnistylePlugin;不进行其中任一集成,样式就不会响应变化。babel.config.js 的 plugins 中列有 babel-plugin-react-compiler 的裸 RN 应用必须保留 Babel 插件
  4. 只能从 react-native-unistyles 导入 StyleSheet——它会为所有 RN StyleSheet API(hairlineWidth、compose、flatten、absoluteFill 等)添加 polyfill,因此请移除所有 import { StyleSheet } from 'react-native'
  5. 优先直接从 react-native-unistyles 导入 StyleSheet——如果通过自己的包或别名(例如 @myorg/design-system)重新导出它,则必须通过 autoProcessImports 列出使用它的 root 之外的文件
  6. 必须在访问样式之前,在组件渲染中调用 styles.useVariants({...})(始终传入对象)
  7. 必须使用 React 19+——在较旧的 React 版本中,Unistyles 会在导入时抛出错误
  8. 绝不能同时使用 initialTheme 和 adaptiveThemes: true——StyleSheet.configure 会抛出错误

参考文件

评论

支持 Markdown