Chapter 03

组件与 Props

SolidJS 组件只运行一次、props 是响应式代理——掌握与 React 的关键差异

1. 函数组件:只运行一次

SolidJS 中的函数组件与 React 最大的区别是:组件函数只在初始化时执行一次,之后不会因为状态变化而重新执行(re-render)。

当 Signal 变化时,SolidJS 只会重新执行对应的 Effect 或 Memo,不会重新调用组件函数。这意味着:

function ExpensiveComponent() {
  // 这行只执行一次!不管后续 Signal 如何变化
  const config = buildExpensiveConfig(); // 安全,不需要 useMemo

  const [count, setCount] = createSignal(0);

  return <p>{count()}</p>;
  // count() 变化时,只更新这个文本节点,不重跑组件函数
}

2. Props:响应式代理

SolidJS 的 props 是一个响应式代理对象(Proxy),而不是普通的 JavaScript 对象。通过 props.xxx 访问属性时会触发响应式追踪——当父组件传入的值变化时,子组件中使用该 prop 的地方会自动更新。

正确访问 props

interface ButtonProps {
  label: string;
  color?: string;
  onClick: () => void;
}

function Button(props: ButtonProps) {
  // ✅ 正确:在 JSX 或 Effect 中直接访问 props.xxx
  return (
    <button
      style={{ color: props.color }}
      onClick={props.onClick}
    >
      {props.label}
    </button>
  );
}

function BadButton(props: ButtonProps) {
  // ❌ 错误:解构 props 会失去响应性!
  const { label, color } = props; // label 现在只是一个普通字符串
  return <button style={{ color }}>{label}</button>;
  // 当父组件更新 label 时,这里不会响应式更新!
}
🚨

SolidJS 最重要的规则:永远不要解构 props。这是从 React 迁移到 SolidJS 最容易犯的错误。如果你在函数参数处写 function Btn({ label }),label 会立即变成一个普通值,失去响应性。

3. children prop

SolidJS 的 children 与 React 类似,但需要用 children helper 函数来处理(因为 children 本身也是响应式的):

import { children } from "solid-js";
import type { JSX } from "solid-js";

interface CardProps {
  title: string;
  children: JSX.Element;
}

function Card(props: CardProps) {
  // children() helper 正确处理响应式子节点
  const c = children(() => props.children);

  return (
    <div class="card">
      <h3>{props.title}</h3>
      <div class="card-body">{c()}</div>
    </div>
  );
}

// 使用
<Card title="我的卡片">
  <p>这是卡片内容</p>
</Card>

4. mergeProps — 安全合并默认值

mergeProps 以响应式安全的方式合并多个 props 对象,常用于设置默认值:

import { mergeProps } from "solid-js";

interface ButtonProps {
  label?: string;
  variant?: "primary" | "secondary";
  disabled?: boolean;
}

function Button(props: ButtonProps) {
  // 安全地设置默认值,保持响应性
  const merged = mergeProps(
    { label: "按钮", variant: "primary", disabled: false },
    props
  );

  return (
    <button
      class={`btn btn-${merged.variant}`}
      disabled={merged.disabled}
    >
      {merged.label}
    </button>
  );
}

// 注意:不能用这种方式——这会破坏响应性!
// const label = props.label ?? "按钮"; // ❌ 只读一次

5. splitProps — 分离 props

splitProps 将 props 按照指定 key 分成两个部分,常用于将"自身 props"和"透传 props"分离:

import { splitProps } from "solid-js";

interface InputProps {
  label: string;
  error?: string;
  // 还有很多原生 input 属性...
  [key: string]: any;
}

function FormInput(props: InputProps) {
  // 分离自定义 props 和原生 input props
  const [local, inputProps] = splitProps(props, ["label", "error"]);

  return (
    <div>
      <label>{local.label}</label>
      {/* inputProps 包含所有其他属性,如 type, placeholder, value 等 */}
      <input {...inputProps} />
      {local.error && <span class="error">{local.error}</span>}
    </div>
  );
}

// 使用
<FormInput
  label="用户名"
  error="必填项"
  type="text"
  placeholder="请输入用户名"
/>

6. 组件生命周期:onMount 与 onCleanup

SolidJS 通过两个函数提供基本的生命周期钩子:

import { onMount, onCleanup, createSignal } from "solid-js";

function CanvasChart() {
  let canvasRef: HTMLCanvasElement;
  const [data, setData] = createSignal([]);

  // 挂载后初始化 canvas
  onMount(() => {
    const ctx = canvasRef.getContext("2d");
    drawChart(ctx, data());
    console.log("Canvas mounted, size:", canvasRef.width);
  });

  // WebSocket 连接示例
  onMount(() => {
    const ws = new WebSocket("wss://api.example.com");
    ws.onmessage = (e) => setData(JSON.parse(e.data));

    // 组件卸载时自动关闭连接
    onCleanup(() => ws.close());
  });

  return <canvas ref={canvasRef!} width="400" height="300" />;
}

7. 实战:可复用卡片组件

import { splitProps, mergeProps, children } from "solid-js";
import type { JSX } from "solid-js";

interface CardProps {
  title?: string;
  variant?: "default" | "primary" | "danger";
  collapsible?: boolean;
  children: JSX.Element;
  class?: string;
}

function Card(props: CardProps) {
  const merged = mergeProps({ variant: "default", collapsible: false }, props);
  const [local, rest] = splitProps(merged, ["title", "variant", "collapsible", "children"]);
  const c = children(() => local.children);
  const [open, setOpen] = createSignal(true);

  return (
    <div class={`card card-${local.variant} ${rest.class ?? ""}`}>
      {local.title && (
        <div class="card-header" onClick={() => local.collapsible && setOpen(v => !v)}>
          <h3>{local.title}</h3>
          {local.collapsible && <span>{open() ? "▲" : "▼"}</span>}
        </div>
      )}
      {open() && <div class="card-body">{c()}</div>}
    </div>
  );
}

本章小结:SolidJS 组件只执行一次,props 是响应式代理不能解构,mergeProps 安全合并默认值,splitProps 分离属性,onMount/onCleanup 提供生命周期控制。掌握这些规则,你就避免了 80% 的 SolidJS 新手错误。