SolidJS 1.0在今年6月正式发布后,我就一直在关注。它的性能很强,API设计也很优雅,号称"拥有React的开发体验和Svelte的运行时性能"。上个月我用SolidJS做了一个中小型项目,整体体验不错,但也踩了不少坑。本文记录实战中遇到的问题和解决方案,给想用SolidJS的朋友一个参考。

一、为什么选SolidJS

在选技术栈的时候,我对比了React、Vue 3、Svelte和SolidJS。最终选SolidJS的原因有几个:

  1. 性能好。 SolidJS采用细粒度的响应式更新,不需要虚拟DOM,运行时性能接近原生JS,在JS框架性能 benchmark中名列前茅。
  2. API熟悉。 SolidJS的API和React很像,有JSX、Hooks、组件化,有React经验的人上手很快。
  3. 包体积小。 Hello World只有不到5KB,比React小很多,对首屏加载友好。
  4. TypeScript支持好。 SolidJS对TypeScript的支持很完善,类型推导很智能。

项目做下来,这些优点都感受到了。但也遇到了不少坑,下面一个个说。

二、响应式的坑

SolidJS的响应式是核心,也是最容易踩坑的地方。

1. 解构props会丢失响应式。

这是最常见的坑。在React中,我们习惯解构props:

function MyComponent({ name, age }) {
  return <div>{name} - {age}</div>
}

但在SolidJS中,这样写会丢失响应式。因为props是一个响应式对象,解构之后,变量就变成了普通的值,不会再随props更新。

正确的写法是不解构,直接用props.nameprops.age。或者用splitProps来拆分props,保持响应式:

function MyComponent(props) {
  const [local, others] = splitProps(props, ['name', 'age'])
  return <div>{local.name} - {local.age}</div>
}

这个坑我踩了好几次,刚开始总是忘记,导致组件不更新。

2. 在createEffect中访问信号要注意依赖。

SolidJS的createEffect会自动追踪依赖,在依赖变化时重新执行。但如果在effect中访问了条件分支里的信号,依赖追踪可能不符合预期。

比如:

createEffect(() => {
  if (show()) {
    console.log(name())
  }
})

show()为false时,name()不会被访问,所以不会被追踪为依赖。当show()变成true时,effect会重新执行,这时候name()才会被追踪。这个行为其实是合理的,但刚开始用的时候容易困惑。

3. 不要在渲染期间创建信号。

在组件渲染过程中创建信号(createSignal)是可以的,但不要在条件分支或循环中创建,否则会导致不一致。信号应该在组件顶层创建。

4. store的更新是不可变的。

SolidJS的store(createStore)看起来可以直接修改,但实际上是不可变更新。比如:

const [state, setState] = createStore({ count: 0 })
// 正确
setState('count', c => c + 1)
// 错误,不会触发更新
state.count++

刚开始我习惯性地直接修改state,发现界面不更新,查了文档才知道要用setState。

三、组件生命周期的坑

1. 没有componentDidMount,用onMount。

SolidJS没有React的componentDidMount,对应的是onMount。onMount在组件挂载到DOM之后执行,可以用来获取DOM元素、发起请求等。

function MyComponent() {
  let ref
  onMount(() => {
    console.log(ref) // 可以获取到DOM元素
  })
  return <div ref={ref}>Hello</div>
}

2. onCleanup的执行时机。

onCleanup在组件卸载时执行,也会在effect重新执行之前执行。刚开始我以为只有卸载时才执行,后来发现在effect的清理函数中也会用到。

3. 组件函数只执行一次。

这是SolidJS和React最大的区别之一。React的组件函数会在每次渲染时重新执行,而SolidJS的组件函数只执行一次。这意味着你不能在组件函数中写副作用,也不能用普通变量来存储会变化的状态。

刚开始我很不适应,因为React的思维模式已经根深蒂固了。比如我想在组件中计算一个值,习惯性地写:

function MyComponent() {
  const [count, setCount] = createSignal(0)
  const doubled = count() * 2 // 这是错的,doubled不会更新
  return <div>{doubled}</div>
}

正确的写法是用createMemo

const doubled = createMemo(() => count() * 2)

或者直接在JSX中计算:

return <div>{count() * 2}</div>

这个坑踩了好几次之后,才慢慢适应了SolidJS的思维模式。

四、状态管理的坑

1. 全局状态管理方案不成熟。

React有Redux、Zustand、Jotai等成熟的状态管理库,SolidJS的生态还比较新,全局状态管理的选择不多。

官方推荐用createContext+createStore来做全局状态,但用起来没有Redux那么方便。我在项目中自己封装了一个简单的store,基本够用,但不如成熟的库完善。

2. 跨组件共享状态要注意。

在SolidJS中,信号可以直接在组件外创建,然后在多个组件中共享。这比React的Context简单,但也要注意不要滥用,否则状态流向会不清晰。

我的建议是:组件内部状态用createSignal,跨组件状态用Context,全局状态用单独的store模块。

五、JSX的坑

1. class和className。

SolidJS的JSX用class而不是className,这和React不一样。刚开始总是写错,写成className,发现样式不生效,查了半天才发现。

2. 样式绑定。

SolidJS的样式绑定和React不一样,style属性接受一个对象,而且是响应式的:

<div style={{ color: color(), 'font-size': '14px' }}>

注意属性名要用kebab-case,不能用camelCase。

3. ref的用法。

SolidJS的ref和React类似,但有一个区别:SolidJS的ref可以是一个函数,在元素创建时执行:

<div ref={el => console.log(el)}>

这个特性有时候很方便,但也要注意,函数ref会在每次渲染时执行(虽然SolidJS组件只渲染一次)。

4. 列表渲染用For组件。

SolidJS的列表渲染用<For>组件,而不是map:

<For each={items()}>{item => <li>{item}</li>}</For>

For组件会做diff,只更新变化的项,性能很好。但要注意,each必须是一个响应式的值,如果是普通数组,就不会更新。

六、生态和工具链的坑

1. 生态还不够丰富。

SolidJS还很新,生态不如React丰富。很多常用的库没有SolidJS版本,比如一些UI组件库、图表库、表单库等。

我在项目中需要一个表格组件,找了一圈没有特别满意的,最后自己封装了一个简单的。如果你需要很多第三方库,可能会遇到找不到的情况。

2. 调试工具不完善。

React有React DevTools,可以查看组件树、状态、props等。SolidJS的DevTools还比较初级,功能有限。调试的时候主要靠console.log,效率不如React。

3. 构建工具配置。

SolidJS推荐用Vite,配置很简单。但如果要和其他工具集成(比如ESLint、Prettier、TypeScript),需要自己配置,没有React那么多现成的脚手架。

七、性能方面的坑

1. 不要过早优化。

SolidJS的性能已经很好了,不需要过早优化。我刚开始用的时候,总是担心性能,到处用createMemo、splitProps,后来发现很多都是多余的,反而增加了代码复杂度。

我的建议是:先写正常的代码,遇到性能问题再优化。SolidJS的默认性能已经足够好了。

2. 注意信号的粒度。

SolidJS的细粒度更新是优势,但如果信号太粗,也会导致不必要的更新。比如一个大对象用一个信号存储,修改任何属性都会触发所有依赖更新。这时候应该用store(createStore),它支持细粒度的属性更新。

3. 避免在JSX中创建函数。

在JSX中内联创建函数,每次渲染都会创建新的函数引用。虽然SolidJS组件只渲染一次,但在For循环中,每个列表项都会创建函数,可能会影响性能。可以把函数提取到组件外面。

八、项目总结

用SolidJS做完这个项目,整体感受是:

优点:

  • 性能确实好,页面很流畅
  • 包体积小,首屏加载快
  • API设计优雅,写起来很舒服
  • TypeScript支持好

缺点:

  • 生态不够丰富,很多库要自己写
  • 调试工具不完善
  • 学习曲线比预期陡,主要是思维模式的转变
  • 社区资料少,遇到问题不容易搜到答案

如果让我再选一次,对于中小型项目,我还是会考虑SolidJS,尤其是对性能要求高的项目。但对于大型项目,可能还是会选React,因为生态和工具链更成熟。

九、给想用SolidJS的人的建议

  1. 先理解响应式原理。 SolidJS的核心是细粒度响应式,先搞懂它的原理,再写代码,会少踩很多坑。
  2. 忘掉React的思维模式。 虽然API像React,但运行机制完全不同。组件只执行一次,没有虚拟DOM,这些都要适应。
  3. 多读官方文档。 SolidJS的官方文档写得很好,而且是最权威的资料。遇到问题先查文档。
  4. 从小项目开始。 不要一上来就用SolidJS做大型项目,先做个小项目练练手,熟悉了再考虑大项目。
  5. 关注生态。 选技术栈之前,先看看你需要的库有没有SolidJS版本,避免做到一半发现没有合适的库。

十、写在最后

SolidJS是一个很有潜力的前端框架,它的性能和开发体验都很好。虽然生态还不够成熟,但随着1.0的发布,社区正在快速成长。

踩坑是学习新技术的必经之路,这些坑让我更深入地理解了SolidJS的设计理念。如果你也在考虑用SolidJS,希望我的经验能帮你少走一些弯路。

最后想说,框架只是工具,没有最好的框架,只有最适合的框架。根据项目需求和团队情况来选择,才是最理性的做法。