For AI agents: the complete documentation index is available at /next/zh/llms.txt, the full documentation bundle is available at /next/zh/llms-full.txt, and this page is available as Markdown at /next/zh/react/best-practices.md.
  • 简体中文
  • 最佳实践

    Agent Skill 可用

    这些最佳实践也可作为 Agent Skill 提供给你的 coding agent。使用 npx skills add lynx-community/skills 安装,即可在编辑器中获得自动检测和自动修复能力。

    本页介绍使用 ReactLynx 开发时的常见模式和陷阱,重点关注 Lynx 的双线程架构。

    原生 API 调用使用 'background only'

    严重程度:关键

    在 Lynx 的双线程架构中,主线程负责 React 组件渲染和 JSX 求值,后台线程管理 effects、事件处理和原生模块调用。在主线程上调用 lynx.getJSModuleNativeModules 等原生 API 会导致 UI 阻塞、同步开销和用户体验下降。

    错误示例

    // ❌ 在组件顶层调用原生 API — 运行在主线程上
    function App() {
      const data = NativeModules.Storage.get('key');
      return <text>{data}</text>;
    }
    // ❌ 在渲染逻辑中调用原生 API
    function App() {
      return <text>{lynx.getJSModule('GlobalEventEmitter').emit('event')}</text>;
    }

    正确示例

    原生模块调用在后台线程执行的上下文中是允许的:

    // ✅ 在 useEffect 中 — 运行在后台线程上
    function App() {
      const [data, setData] = useState('');
    
      useEffect(() => {
        const value = NativeModules.Storage.get('key');
        setData(value);
      }, []);
    
      return <text>{data}</text>;
    }
    // ✅ 在事件处理函数中 — 运行在后台线程上
    function App() {
      function handleTap() {
        lynx.getJSModule('GlobalEventEmitter').emit('event');
      }
    
      return (
        <view bindtap={handleTap}>
          <text>点击我</text>
        </view>
      );
    }
    // ✅ 使用 'background only' 指令
    function fetchData() {
      'background only';
      return NativeModules.Storage.get('key');
    }

    允许的上下文

    原生模块调用在以下上下文中是安全的:

    • useEffect / useLayoutEffect hooks
    • useImperativeHandle 实现
    • Ref 回调
    • 事件处理函数(bindtap 等)
    • 使用 'background only' 指令标记的函数

    'background only' 指令

    当包含原生调用的函数不自然地属于上述上下文之一时,将 'background only' 指令作为函数体的第一条语句:

    function sendAnalytics(eventName: string) {
      'background only';
      lynx.getJSModule('Analytics').track(eventName);
    }

    单引号和双引号均可:'background only'"background only"

    正确使用事件处理函数

    严重程度:中

    ReactLynx 中的事件处理函数运行在后台线程上,因此可以安全地进行原生模块调用、API 请求和繁重计算。

    事件处理类型

    属性行为使用场景
    bindtap冒泡到父元素tap 事件的默认选择
    catchtap阻止传播防止父元素处理函数触发

    基本用法

    推荐使用函数引用而非内联函数以获得更好的性能:

    // ✅ 函数引用(推荐)
    function App() {
      function handleTap(event) {
        console.log('Target id:', event.target.id);
      }
    
      return (
        <view id="my-view" bindtap={handleTap}>
          点击我
        </view>
      );
    }

    事件传播

    使用 bindtap 冒泡:

    function App() {
      function handleOuterTap() {
        console.log('outer tapped'); // 第二个触发
      }
      function handleInnerTap() {
        console.log('inner tapped'); // 第一个触发
      }
    
      return (
        <view bindtap={handleOuterTap}>
          <view bindtap={handleInnerTap}>内部</view>
        </view>
      );
    }
    // 点击内部输出:"inner tapped"、"outer tapped"

    使用 catchtap 阻止传播:

    function App() {
      function handleOuterTap() {
        console.log('outer tapped'); // 不会触发
      }
      function handleInnerTap() {
        console.log('inner tapped'); // 只有这个触发
      }
    
      return (
        <view bindtap={handleOuterTap}>
          <view catchtap={handleInnerTap}>内部</view>
        </view>
      );
    }

    使用 dataset

    通过 data- 属性传递自定义数据,而非使用闭包:

    function App() {
      function handleTap(event) {
        const { itemId, itemName } = event.currentTarget.dataset;
        console.log(`Tapped item: ${itemId} - ${itemName}`);
      }
    
      return (
        <view data-item-id="123" data-item-name="Product" bindtap={handleTap}>
          点击我
        </view>
      );
    }

    要点

    1. 使用函数引用而非内联箭头函数以获得更好的性能
    2. 仅在需要阻止事件传播时使用 catchtap
    3. 尽可能使用 dataset 传递数据而非闭包
    4. 优先使用 currentTarget 而非 target 来访问监听元素的数据

    使用主线程脚本实现流畅动画

    严重程度:中

    在 Lynx 的多线程架构中,事件在主线程上触发,但常规 JS 事件处理函数在后台线程上执行。这种往返会导致动画和手势处理中明显的延迟。主线程脚本提供主线程上的同步事件处理,消除响应延迟。

    错误示例

    // ❌ 滚动处理在后台线程上 — 动画滞后于手势
    function App() {
      const [pos, setPos] = useState(0);
    
      function onScroll(event) {
        setPos(event.detail.scrollTop);
      }
    
      return (
        <scroll-view global-bindscroll={onScroll}>
          <view style={{ transform: `translateY(${pos}px)` }} />
        </scroll-view>
      );
    }

    正确示例

    // ✅ 滚动处理在主线程上 — 无延迟
    function App() {
      function onScroll(event: MainThread.IScrollEvent) {
        'main thread';
        const scrollTop = event.detail.scrollTop;
        event.currentTarget.setStyleProperty(
          'transform',
          `translateY(${scrollTop}px)`,
        );
      }
    
      return (
        <scroll-view main-thread:global-bindscroll={onScroll}>
          <view />
        </scroll-view>
      );
    }

    使用方法

    1. 为事件属性添加 main-thread: 前缀:
    <view main-thread:bindtap={onTap} />
    1. 使用 'main thread' 指令声明函数:
    function onTap(event: MainThread.ITouchEvent) {
      'main thread';
      event.currentTarget.setStyleProperty('background-color', 'red');
    }

    使用 main-thread:ref

    使用 useMainThreadRef() 获取可在主线程上使用的节点对象:

    import { useMainThreadRef } from '@lynx-js/react';
    
    function App() {
      const textRef = useMainThreadRef<MainThread.Element>();
    
      function handleTap(event: MainThread.ITouchEvent) {
        'main thread';
        textRef.current?.setStyleProperty('background-color', 'red');
      }
    
      return (
        <view main-thread:bindtap={handleTap}>
          <text main-thread:ref={textRef}>这段文字会变</text>
        </view>
      );
    }

    在主线程中维护状态

    使用 MainThreadRef 在主线程函数调用之间持久化状态:

    import { useMainThreadRef } from '@lynx-js/react';
    
    function App() {
      const countRef = useMainThreadRef(0);
    
      function handleTap(event: MainThread.ITouchEvent) {
        'main thread';
        countRef.current++;
        event.currentTarget.setStyleProperty(
          'background-color',
          countRef.current % 2 ? 'blue' : 'green',
        );
      }
    
      return <view main-thread:bindtap={handleTap} />;
    }

    规则

    规则描述
    必须使用 'main thread' 指令函数体的第一条语句
    使用 main-thread: 前缀绑定事件例如 main-thread:bindtap
    捕获的变量必须可 JSON 序列化通过 JSON.stringify() 传递
    不能修改捕获的变量主线程中只能只读访问
    MainThreadRef.current 只能在主线程中访问使用 useMainThreadRef()

    提升静态 JSX 元素

    严重程度:低

    当 JSX 元素在组件函数内部定义时,它们会在每次渲染时重新创建。对于永远不会改变的静态元素,这是不必要的开销。将它们提取为组件外部的常量。

    错误示例

    // ❌ 静态元素在每次渲染时重新创建
    function App({ title }) {
      return (
        <view>
          <text>{title}</text>
          <view className="animate-pulse h-20 bg-gray-200" />
        </view>
      );
    }

    正确示例

    // ✅ 静态元素提升到外部 — 跨渲染复用
    const loadingSkeleton = <view className="animate-pulse h-20 bg-gray-200" />;
    
    function App({ title }) {
      return (
        <view>
          <text>{title}</text>
          {loadingSkeleton}
        </view>
      );
    }

    这对于大型静态 SVG 节点尤其有帮助,因为重新创建它们的成本很高。

    Note

    如果你的项目使用了 React Compiler,它会自动提升静态 JSX 元素并优化组件重新渲染,无需手动提升。

    除非另有说明,本项目采用知识共享署名 4.0 国际许可协议进行许可,代码示例采用 Apache License 2.0 许可协议进行许可。