1. 前言
Solid.js,一个比 React 更 react 的框架。每一个使用 React 的同学,你可以不使用,但不应该不了解。
目前 Solid.js 发布了最新的官方文档,但却缺少对应的中文文档。为了帮助大家学习 Solid.js,为爱发电翻译文档。
我同时搭建了 Solid.js 最新的中文文档站点:solid.yayujs.com ,欢迎勘误。
虽说是翻译,但个人并不喜欢严格遵守原文,为了保证中文阅读流畅,会删减部分语句,对难懂的部分也会另做补充解释,希望能给大家带来一个好的中文学习体验。
欢迎围观我的“朋友圈”、加入“低调务实优秀中国好青年”前端社群,分享技术,带你成长。
2. TypeScript
TypeScript 是 JavaScript 的超集,它通过引入静态类型增强了代码的可靠性和可预测性。虽然 JavaScript 代码可以直接在 TypeScript 中使用,但 TypeScript 中添加的类型注释提供了更清晰的代码结构和文档,使开发人员更容易理解。
通过借助标准的 JSX(JavaScript 的语法扩展),Solid 实现了对 TypeScript 的无缝解释。Solid 还为 API 内置了类型,以提高准确性。
对于急于开始的开发者,我们在 GitHub 上提供了 TypeScript 模板。
3. 配置 TypeScript
在将 TypeScript 与 Solid JSX 编译器集成时,需要进行一些设置以实现无缝交互:
tsconfig.json
中的"jsx": "preserve"
保持原始的 JSX 形式。这是因为 Solid 的 JSX 转换与 TypeScript 的 JSX 转换不兼容。"jsxImportSource": "solid-js"
将 Solid 指定为 JSX 类型的来源。
对于基本设置,您的 tsconfig.json
应该类似于:
{
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "solid-js"
}
}
对于具有多种 JSX 源的项目,例如 React 和 Solid 混合使用,存在一定的灵活性。虽然可以在 tsconfig.json
中设置默认的 jsxImportSource
,这将对应于大多数文件,但 TypeScript 也允许文件级别的覆盖。在 .tsx
文件中使用特定的 pragma 可以实现这一点:
/** @jsxImportSource solid-js */
如果使用 React:
/** @jsxImportSource react */
选择 React JSX pragma 意味着将 React 及其相关依赖完全集成到项目中。此外,它还确保项目架构为处理 React JSX 文件做好准备,这一点至关重要。
4. 从 JavaScript 迁移到 TypeScript
从 JavaScript 过渡到 TypeScript ,可以提供静态类型的好处。如果要迁移到 Typescript:
- 将 TypeScript 安装到您的项目中。
# npm
npm i --save-dev typescript
# yarn
yarn add --dev typescript
# pnpm
pnpm add --save-dev typescript
# bun
bun add --save-dev typescript
- 运行以下命令生成
tsconfig.json
文件。
# npm
npx tsc --init
# yarn
yarn dlx tsc --init
# pnpm
pnpm tsc --init
# bunx
bunx tsc --init
- 更新
tsconfig.json
的内容以匹配 Solid 的配置:
{
"compilerOptions": {
"strict": true,
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"jsx": "preserve",
"jsxImportSource": "solid-js",
"types": ["vite/client"],
"noEmit": true,
"isolatedModules": true
}
}
- 创建 TypeScript 或
.tsx
文件测试设置。
import { type Component } from "solid-js";
const MyTsComponent(): Component = () => {
return (
<div>
<h1>This is a TypeScript component</h1>
</div>
);
}
export default MyTsComponent;
如果使用现有的 JavaScript 组件,请导入 TypeScript 组件:
import MyTsComponent from "./MyTsComponent";
function MyJsComponent() {
return (
<>
{/* ... */}
<MyTsComponent />
</>
);
}
说明:
如果您希望将入口点文件从
index.jsx
更改为index.tsx
,需要修改<script>
中的src
属性,如下所示:
<!doctype html>
<html lang="en">
<head>
<!-- ... -->
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script src="/src/index.tsx" type="module"></script>
</body>
</html>
5. API 类型
Solid 是用 TypeScript 编写的,这意味着所有内容都是开箱即用的。
侧边栏中的“参考”选项卡提供了 API 调用类型的详细介绍。此外,还有几个有用的定义可以更轻松地声明显式类型。
5.1. Signals
使用 createSignal<T>
,signal 的类型可以定义为 T
。
const [count, setCount] = createSignal<number>();
此时 createSignal
具有返回类型 Signal<number | undefined>
,它对应于传入的类型,以及 undefined
(因为它未初始化)。返回的是一个 getter-setter
元组,两者都是泛型类型:
import type { Signal, Accessor, Setter } from "solid-js";
type Signal<T> = [get: Accessor<T>, set: Setter<T>];
在 Solid 中,signal 的 getter,如 count
,本质上是一个返回特定类型的函数。在本例中,类型为 Accessor<number | undefined>
,它转换为函数 () => number | undefined
。由于 signal 未初始化,其初始状态为 undefined
,因此 undefined
包含在其类型中。
对应的 setter setCount
具有更复杂的类型:
Setter<number | undefined>.
本质上,这种类型意味着该函数可以接受一个直接的数字或另一个函数作为其输入。如果提供了一个函数,该函数可以将 signal 之前的值作为其参数并返回一个新值。初始值和结果值都可以是数字或 undefined
。重要的是,调用不带任何参数的 setCount
会将 signal 的值重置为 undefined
。
当使用 setter 的函数形式时,signal 的当前值将始终作为唯一参数传递给回调。此外,setter 的返回类型将与传递给它的值的类型保持一致,这与典型的赋值操作的预期行为相呼应。
如果 signal 旨在存储函数,setter 不会直接接受新函数作为值。这是因为它无法区分是否应该执行该函数以产生实际值或按原样存储它。在这些情况下,建议使用 setter 的回调形式:
setSignal(() => value);
5.1.1. 默认值
通过在调用 createSignal
时提供默认值,可以避免显式类型指定的需要,并消除 | undefined
类型的可能性。这是利用类型推断来自动确定类型:
const [count, setCount] = createSignal(0);
const [name, setName] = createSignal("");
在此示例中,TypeScript 将类型理解为 number
和 string
。这意味着 count
和 name
分别直接接收类型 Accessor<number>
和 Accessor<string>
,而无需 | undefined
标记。
5.2. Context
正如 signal 使用 createSignal<T>
一样,上下文使用 createContext<T>
,将上下文值的类型 T
作为参数:
type Data = { count: number; name: string };
调用 useContext(dataContext)
时,将返回上下文中包含的类型。例如,如果上下文是 Context<Data | undefined>
,则使用 useContext
时将返回 Data | undefined
类型。 | undefined
表示上下文可能在组件的祖先层级结构中未定义。
dataContext
将被 Solid 理解为 Context<Data | undefined>
。调用 useContext(dataContext)
反映了这个类型,返回 Data | undefined
。当上下文的值将被使用但无法确定时,就会出现| undefined
。
与 signal 中的默认值非常相似,可以通过给出默认值来避免类型中的 | undefined
,如果上下文 provider 未分配任何值,则将返回该默认值:
const dataContext = createContext({ count: 0, name: "" });
通过提供默认值,TypeScript 确定 dataContext
为 Context<{ count: number, name: string }>
。这等同于 Context<Data>
但不包含 | undefined
。
一种常见的方法是用一个工厂函数来生成上下文的值。通过使用 TypeScript 的 ReturnType ,您可以使用此函数的返回类型来为上下文指定类型:
export const makeCountNameContext = (initialCount = 0, initialName = "") => {
const [count, setCount] = createSignal(initialCount);
const [name, setName] = createSignal(initialName);
return [
{ count, name },
{ setCount, setName },
] as const;
};
type CountNameContextType = ReturnType<typeof makeCountNameContext>;
export const CountNameContext = createContext<CountNameContextType>();
CountNameContextType
将对应 makeCountNameContext
的结果:
[
{ count: Accessor<number>, name: Accessor<string> },
{ setCount: Setter<number>, setName: Setter<string> },
];
要获取上下文,使用 useCountNameContext
,它的类型签名为 () => CountNameContextType | undefined
。
在需要避免 undefined
作为可能类型的场景中,断言上下文将始终存在。此外,抛出可读的错误可能比非空断言更可取:
export const useCountNameContext = () => {
const countName = useContext(CountNameContext);
if (!countName) {
throw new Error("useCountNameContext should be called inside its ContextProvider");
}
return countName;
};
注意:虽然为 createContext
提供默认值可以使上下文始终保持定义状态,但这种方法可能并不总是可取的。根据具体用例,这可能导致静默失败,这可能不太可取。
5.3. 组件
5.3.1. 基础知识
默认情况下,Solid 中的组件使用泛型 Component<P>
类型,其中 P
表示 props 的类型,它是一个对象。
import type { Component } from "solid-js";
const MyComponent: Component<MyProps> = (props) => {
...
}
JSX.Element
表示 Solid 可渲染的任何内容,可以是 DOM 节点、JSX 元素数组或生成 JSX.Element
的函数。
尝试传递不必要的 props 或 children 将导致类型错误:
// in counter.tsx
const Counter: Component = () => {
const [count, setCount] = createSignal(0);
return <button onClick={() => setCount((prev) => prev + 1)}>{count()}</button>;
};
// in app.tsx
<Counter />; // ✔️
<Counter initial={5} />; // ❌: No 'initial' prop defined
<Counter>hi</Counter>; // ❌: Children aren't expected
5.3.2. 带 props 的组件
对于需要使用 props 的组件,可以使用泛型进行类型定义:
const InitCounter: Component<{ initial: number }> = (props) => {
const [count, setCount] = createSignal(props.initial);
return <button onClick={() => setCount((prev) => prev + 1)}>{count()}</button>;
};
<InitCounter initial={5} />;
5.3.3. 带 children 的组件
通常,组件可能需要接受子元素。为此,Solid 提供了 ParentComponent
,其中包括 children?
作为可选属性。如果使用 function
关键字定义组件,则 ParentProps
可以用作 props 的辅助工具:
import { ParentComponent } from "solid-js";
const CustomCounter: ParentComponent = (props) => {
const [count, setCount] = createSignal(0);
return (
<button onClick={() => setCount((prev) => prev + 1)}>
{count()}
{props.children}
</button>
);
};
在此示例中, props
被推断为 {children?: JSX.Element }
类型,简化了定义可接受子元素的组件的过程。
5.3.4. 特殊组件类型
Solid 为专门处理子元素的组件提供了子类型:
这些类型确保子元素符合所需类型,保持一致的组件行为。
5.3.4.1. 没有 Component
类型的组件
使用 Component
类型是一个偏好问题,而不是严格的要求。任何接受 props 并返回 JSX.Element 的函数都有资格作为有效组件:
// arrow function
const MyComponent = (props: MyProps): JSX.Element => { ... }
// function declaration
function MyComponent(props: MyProps): JSX.Element { ... }
// component which takes no props
function MyComponent(): JSX.Element { ... }
值得注意的是,Component
类型不能用于创建泛型组件。相反,泛型必须显式地进行类型定义:
// For arrow functions, the syntax <T> by itself is invalid in TSX because it could be confused with JSX.
const MyGenericComponent = <T extends unknown>(props: MyProps<T>): JSX.Element => {
/* ... */
};
// Using a function declaration for a generic component
function MyGenericComponent<T>(props: MyProps<T>): JSX.Element {
/* ... */
}
说明:
每个
Component
类型都有一个对应的 Props 类型,用于定义其属性的形状。这些Props
类型也接受与其关联的Component
类型相同的泛型类型。
5.4. 事件处理
5.4.1. 基础知识
在 Solid 中,事件处理程序的类型指定为 JSX.EventHandler<TElement, TEvent>
。这里,TElement
指的是事件链接到的元素的类型。 TEvent
表示事件本身的类型,可以在代码中替代 (event: TEvent) => void
。此方法保证了事件对象中 currentTarget
和 target
的准确类型定义,同时还消除了对内联事件处理程序的需要。
import type { JSX } from "solid-js";
// Defining an event handler using the `EventHandler` type:
const onInput: JSX.EventHandler<HTMLInputElement, InputEvent> = (event) => {
console.log("Input changed to", event.currentTarget.value);
};
// Then attach handler to an input element:
<input onInput={onInput} />;
5.4.2. 内联处理程序
在 JSX 属性中定义内联事件处理程序会自动提供类型推断和检查,从而无需额外的类型定义工作:
<input
onInput={(event) => {
console.log("Input changed to", event.currentTarget.value);
}}
/>
5.4.3. currentTarget 和 target
在事件委托的上下文中,currentTarget
和 target
之间的区别很重要:
currentTarget
: 表示事件处理程序附加到的 DOM 元素。target
:currentTarget
层次结构中启动事件的任何 DOM 元素。
在类型签名 JSX.EventHandler<T, E>
中, currentTarget
将始终具有 T
类型。然而,目标的类型可以更通用,可能是任何 DOM 元素。对于与输入元素直接关联的特定事件(例如 Input
和 Focus
),目标将具有类型 HTMLInputElement
。
5.5. ref
属性
5.5.1. 基础知识
在没有 TypeScript 的环境中,在 Solid 中使用 ref
属性可以确保相应的 DOM 元素在渲染后分配给变量:
let divRef;
console.log(divRef); // Outputs: undefined
onMount(() => {
console.log(divRef); // Outputs: <div> element
});
return <div ref={divRef} />;
在 TypeScript 环境中,特别是在启用了严格的 null 检查的情况下,为这些变量添加类型可能会很有挑战性。
TypeScript 中的一种安全方法是承认 divRef
最初可能是 undefined
并在访问它时实施检查:
let divRef: HTMLDivElement | undefined;
// This would be flagged as an error during compilation
divRef.focus();
onMount(() => {
if (!divRef) return;
divRef.focus();
});
return <div ref={divRef}>...</div>;
在 onMount
函数的作用域内(该函数在渲染后运行),你可以使用非 null
断言(用感叹号 !
表示):
onMount(() => {
divRef!.focus();
});
另一种方法是在赋值阶段绕过 null
,然后在 ref
属性中应用明确的赋值断言:
let divRef: HTMLDivElement;
// Compilation error as expected
divRef.focus();
onMount(() => {
divRef.focus();
});
return <div ref={divRef!}>...</div>;
在这种用例下,在 ref
属性中使用 divRef!
向 TypeScript 表明 divRef
将在此阶段之后接收赋值,这更符合 Solid 的工作方式。
说明
虽然 TypeScript 确实可以捕获在 JSX 块定义之前发生的 refs 的错误用法,但它目前无法识别 Solid 中某些嵌套函数内未定义的变量。因此,在 createMemo 、 createRenderEffect 和 createComputed 等函数中使用 ref 时需要格外小心。
最后,一种风险更大的方法是在变量初始化时使用明确的赋值断言。虽然此方法绕过了 TypeScript 对特定变量的赋值检查,但它提供了一个快速但安全性较低的解决方案,可能会导致运行时错误。
let divRef!: HTMLDivElement;
// Permitted by TypeScript but will throw an error at runtime:
// divRef.focus();
onMount(() => {
divRef.focus();
});
6. 基于控制流的收窄
基于控制流的收窄是指通过使用控制流语句细化值的类型。
考虑这个例子:
const user: User | undefined = maybeUser();
return <div>{user && user.name}</div>;
然而,在 Solid 中,访问器不能以类似的方式收窄:
const [user, setUser] = createSignal<User>();
return <div>{user() && user().name}</div>;
// ^ Object may be 'undefined'.
// Using `<Show>`:
return (
<div>
<Show when={user()}>{user().name /* Object is possibly 'undefined' */}</Show>
</div>
);
在这种情况下,使用可选链是一个很好的替代方案:
return <div>{user()?.name}</div>;
// Using `<Show>`:
return (
<div>
<Show when={user()}>{(nonNullishUser) => nonNullishUser().name}</Show>
</div>
);
此方法类似于使用 keyed 选项,但提供了一个访问器来防止每次 when
值更改重新创建子元素。
return (
<div>
<Show keyed when={user()}>
{(nonNullishUser) => nonNullishUser.name}
</Show>
</div>
);
请注意,可选链可能并不总是可行的。例如,当 UserPanel
组件专门需要一个 User
对象时:
return <UserPanel user={user()} />;
// ^ Type 'undefined' is not assignable to type 'User'.
如果可能,请考虑重构 UserPanel
以接受 undefined
。这可以最大限度地减少 user
从 undefined
变为 User
时所需的更改。
否则,使用 Show 的回调形式:
return <Show when={user()}>{(nonNullishUser) => <UserPanel user={nonNullishUser()} />}</Show>;
只要假设有效,类型转换也可以是一种解决方案:
return <div>{user() && (user() as User).name}</div>;
值得注意的是,这样做可能会导致运行时类型错误。当将类型转换值传递给组件时,可能会发生这种情况,组件会丢弃可能为空的信息,然后异步访问它,例如在事件处理程序或超时中,或者在 onCleanup
中。
使用回调形式时, <Show>
仅从 when
中排除 null
、 undefined
和 false
。如果需要区分联合类型中的类型,可以使用 memo 或计算信号作为替代解决方案:
type User = Admin | OtherUser;
const admin = createMemo(() => {
const u = user();
return u && u.type === "admin" ? u : undefined;
});
return <Show when={admin()}>{(a) => <AdminPanel user={a()} />}</Show>;
使用 Show
时,以下替代方法也可行:
<Show
when={(() => {
const u = user();
return u && u.type === "admin" ? u : undefined;
})()}
>
{(admin) => <AdminPanel user={admin()} />}
</Show>
7. 高级 JSX 属性和指令
7.1. 自定义事件处理程序
要在 Solid 中处理自定义事件,你可以使用属性 on:___
。为这些事件添加类型需要扩展 Solid 的 JSX 命名空间。
class NameEvent extends CustomEvent {
type: "Name";
detail: { name: string };
constructor(name: string) {
super("Name", { detail: { name } });
}
}
declare module "solid-js" {
namespace JSX {
interface CustomEvents {
Name: NameEvent; // Matches `on:Name`
}
}
}
// Usage
<div on:Name={(event) => console.log("name is", event.detail.name)} />;
说明:
v1.9.0 版本新增
现在可以使用交集 EventListenerObject & AddEventListenerOptions
来提供监听器选项,如下所示:
import type { JSX } from "solid-js"
const handler: JSX.EventHandlerWithOptions<HTMLDivElement, Event> = {
once: true,
handleEvent: (event) => {
console.log("will fire only once");
},
}
// Usage
<div on:click={handler} />;
说明:
注意:默认情况下,使用
mousemove
等带有on
前缀的原生事件(例如<div on:mousemove={e => {}} />
)将触发 TypeScript 错误。这是因为这些事件不是 Solid 的自定义事件类型定义的一部分。为了解决这个问题,可以扩展CustomEvents
接口以包含来自HTMLElementEventMap
的事件:包含所有原生事件:
declare module "solid-js" {
namespace JSX {
interface CustomEvents extends HTMLElementEventMap {}
}
}
要包含特定的原生事件,您可以选择某些事件(例如 mousemove
和 pointermove
):
declare module "solid-js" {
namespace JSX {
interface CustomEvents extends Pick<HTMLElementEventMap, "mousemove" | "pointermove"> {}
}
}
7.1.1. 强制属性和自定义属性
在 Solid 中,prop:___
指令允许显式属性设置,这对于保留原始数据类型(如对象或数组)非常有用。另一方面,attr:___
指令允许自定义属性,并且对于处理基于字符串的 HTML 属性非常有效。
declare module "solid-js" {
namespace JSX {
interface ExplicitProperties {
count: number;
name: string;
}
interface ExplicitAttributes {
count: number;
name: string;
}
}
}
// Usage
<Input prop:name={name()} prop:count={count()}/>
<my-web-component attr:name={name()} attr:count={count()}/>
7.1.2. 自定义指令
在 Solid 中,可以使用 use:___
属性实现自定义指令,该属性通常接收一个目标元素和一个 JSX 属性值。传统的 Directives
接口直接对这些值进行类型定义(即 <div use:foo={value} />
中 value
的类型)。但是,新的 DirectiveFunctions
接口采用函数类型并从中派生元素和值的有效类型。
还有其他注意事项:
- 指令函数始终接收单个访问器。对于多个参数,语法
<div use:foo={[a, b]} />
是一个选项,并且应该接受元组的访问器。 - 同样的原则也适用于布尔指令(如
<div use:foo />
中所示)以及具有静态值的指令(如<div use:foo={false} />
)。 DirectiveFunctions
可以接受不严格满足类型要求的函数;此类情况将被忽略。
function model(
element: Element, // directives can be used on any HTML and SVG element
value: Accessor<Signal<string>> // second param will always be an accessor in case value being reactive
) {
const [field, setField] = value();
createRenderEffect(() => (element.value = field()));
element.addEventListener("input", (e) => {
const value = (e.target as HTMLInputElement).value;
setField(value);
});
}
declare module "solid-js" {
namespace JSX {
interface Directives {
model: Signal<string>; // Corresponds to `use:model`
}
}
}
// Usage
let [name, setName] = createSignal("");
<input type="text" use:model={[name, setName]} />;
在使用 DirectiveFunctions
时,可以通过详细说明整个函数类型来检查两个参数(如果存在):
function model(element: HTMLInputElement, value: Accessor<Signal<string>>) {
const [field, setField] = value();
createRenderEffect(() => (element.value = field()));
element.addEventListener("input", (e) => setField(e.target.value));
}
function log(element: Element) {
console.log(element);
}
let num = 0;
function count() {
num++;
}
function foo(comp: Element, args: Accessor<string[]>) {
// function body
}
declare module "solid-js" {
namespace JSX {
interface DirectiveFunctions {
model: typeof model;
log: typeof log;
count: typeof count;
foo: typeof foo;
}
}
}
虽然 Directives
接口可以限制通过 JSX 属性传递给指令的值类型,但 DirectiveFunctions
接口可以确保元素和值都符合预期类型,如下所示:
{/* This is correct */}
<input use:model={createSignal('')} />
{/* These will result in a type error */}
<input use:model />
<input use:model={7} />
<div use:model={createSignal('')} />
7.1.2.1. 使用指令解决导入问题
如果指令是从单独的文件或模块导入的,TypeScript 可能会错误地认为它是一种类型而删除导入。
为了防止这种情况:
- 在
babel-preset-typescript
中配置onlyRemoveTypeImports: true
。 - 使用
vite-plugin-solid
时,请在vite.config.ts
中设置solidPlugin({ typescript: { onlyRemoveTypeImports: true } })
。
需要谨慎管理导出类型和导入类型。在导入模块中包含声明可确保 TypeScript 保留指令的导入。 Tree-shaking 工具通常会从最终 bundle 中省略此代码。
import { directive } from "./directives.js"
directive // prevents TypeScript's tree-shaking
<div use:directive />
Solid.js 中文文档
本篇已收录在掘金专栏 《Solid.js 中文文档》,该系列一共 25 篇。下一篇:这个比 React 更 react 的框架 —— Solid.js 最新中文文档来了!
此外我还写过 JavaScript 系列、TypeScript 系列、React 系列、Next.js 系列、VuePress 博客搭建系列等 14 个系列文章, 全系列文章目录:github.com/mqyqingfeng…
通过文字建立交流本身就是一种缘分,欢迎围观我的“朋友圈”、加入“低调务实优秀中国好青年”前端社群,分享技术,带你成长。