SolidJS 的反直觉 createResource API 设计
最近在写一个 RAG 相关的项目后台管理的时候,脑子一热直接用上了 solidjs。本来是想在 svelte 和 solid 两个之间挑一个体验体验社区大吹特吹的无虚拟 DOM 技术。但是看到 svelte 那 sfc 模板语法突然就不爱了,转头一看 solid 的 jsx 觉得眉清目秀的(可能是 React 写多了出现幻觉了)。
匆匆扫了一遍文档,什么 createSignal、createEffect、createMemo 这都老熟人了。然后路由什么的也大差不差,上手还是比较简单的。直到我对接接口获取数据的时候,本来以为也会有个 useAxios 这种库,结果找了一圈发现推荐的是直接用官方的 createResource API 然后我看了一下文档,第一次用起来的时候,发现这玩意你别说还挺别致,这 API 抽象设计的还挺好的不仅仅用来做请求了,一些异步的函数调用值获取也都能用上。
于是呼,我顺着这个思路写了如下代码
import { render } from "solid-js/web";
import {
For,
createResource,
} from "solid-js";
export default function App() {
const [res] = createResource(async () => {
await new Promise((resolve) => setTimeout(resolve, 1000)); // 假装请求数据
// return [];
return ["aa", "bbb"];
});
return (
<div>
{res.error && <p>error: ${res.error}</p>}
{!res.loading && !res.error && (res() ?? []).length < 1 && (
<p>data is empty</p>
)}
<For each={res() ?? []}>{(item) => <p>{item}</p>}</For>
<p>{`paginator, total: ${(res() ?? []).length}`}</p>
</div>
);
}
render(() => <App />, document.getElementById("app")!);
好的,我们能看到正常情况和数据为空的情况下一切安好。


没问题,这时候我就提交代码发布版本了(获取数据失败的场景我居然没测试,我是罪人我罪该万死)。然后过了两天后端接口崩了。然后我就发现,哎我错误提示哪去了?

于是乎我理所当然的就想打印一下看看 res.error 是什么个情况呗?然后我就眼疾手快的写了如下代码
import { render } from "solid-js/web";
import { For, createResource, createEffect } from "solid-js";
export default function App() {
const [res] = createResource(async () => {
await new Promise((resolve) => setTimeout(resolve, 1000)); // 假装请求数据
throw new Error("it was a naughty mistake");
// return [];
// return ["aa", "bbb"];
});
createEffect(() => {
console.log('res:', res(), 'state:', res.state, 'loading:', res.loading, 'error:', res.error);
})
return (
<div>
{res.error && <p>error: ${res.error}</p>}
{!res.loading && !res.error && (res() ?? []).length < 1 && (
<p>data is empty</p>
)}
<For each={res() ?? []}>{(item) => <p>{item}</p>}</For>
<p>{`paginator, total: ${(res() ?? []).length}`}</p>
</div>
);
}
render(() => <App />, document.getElementById("app")!);结果一看,我嘞个去。不是兄弟你怎么卡在 `pending` 状态不动了?(其实当时的布局写的比这复杂很多,我刚开始还以为我哪个布局写的有问题,于是逐行注释分析,结果删到代码就剩这几行了还是这样)

然后我开始怀疑自我了,难道说是 solidjs 有 bug ?于是我赶紧上 github issue 找了一下,果然有人遇到和我相同的问题 https://github.com/solidjs/solid/issues/1934
刚开始,看这个 issue 的时候其实我还是没有理解本质。只是看到评论说使用 ErrorBoundary 套上就能用了。于是我有样学样的给视图加上 ErrorBoundary,这样错误视图是显示出来了。但是我 createEffect 里获取到的状态还一直是 `pending` 啊,难道这没有问题吗?于是我忍不住回复了 issue,幸运的是到晚上 @madaxen86 回复了我,并指出来 res() 获取值的时候是会抛异常的
那一瞬间,我心路历程是从 懵逼到 质疑到 释然。直到我又反复去调试了几遍,作者这么设计好像也没毛病?一切仿佛是自己的惯性思维导致的。现在再回头去看控制台其实一直有非常明显的报错,已经说明了没有捕获错误,只是自己习惯性没有在意(以为是框架里面抛出来的)。
好了,问题也找到最终原因了。那么博客到这里就结束了?不!这样的话就没必要水一篇博客了,虽然他很有道理但是我才不喜欢这种不符合自己直觉的设计,不喜欢怎么办?当然是重写它啦!于是我覆写了一个 createUnResource 函数来专门安全的获取数据。不需要套 ErrorBoundary 或者取值的时候还要 try catch 了,这才是符合我们 react 男孩的玩具嘛嘿嘿。
/*
* @Author: Bin
* @Date: 2025-12-03
* @FilePath: /utils/createUnResource.ts
*/
import { createResource, type ResourceReturn, type ResourceOptions, type ResourceFetcher, type ResourceSource, type InitializedResourceOptions } from 'solid-js';
type ArgsWithoutSource<T, R> = [fetcher: ResourceFetcher<true, T, R>, options?: ResourceOptions<NoInfer<T>, true>];
type ArgsWithSource<T, S, R> = [source: ResourceSource<S>, fetcher: ResourceFetcher<S, T, R>, options?: ResourceOptions<NoInfer<T>, S>];
type CreateUnResourceArgs<T, S, R> = ArgsWithoutSource<T, R> | ArgsWithSource<T, S, R>;
export function createUnResource<T, R = unknown>(fetcher: ResourceFetcher<true, T, R>, options: InitializedResourceOptions<NoInfer<T>, true>): ResourceReturn<T | undefined, R>;
export function createUnResource<T, R = unknown>(fetcher: ResourceFetcher<true, T, R>, options?: ResourceOptions<NoInfer<T>, true>): ResourceReturn<T | undefined, R>;
export function createUnResource<T, S, R = unknown>(
source: ResourceSource<S>,
fetcher: ResourceFetcher<S, T, R>,
options: InitializedResourceOptions<NoInfer<T>, S>,
): ResourceReturn<T | undefined, R>;
export function createUnResource<T, S, R = unknown>(
source: ResourceSource<S>,
fetcher: ResourceFetcher<S, T, R>,
options?: ResourceOptions<NoInfer<T>, S>,
): ResourceReturn<T | undefined, R>;
export function createUnResource<T, S, R>(...args: CreateUnResourceArgs<T, S, R>): ResourceReturn<T | R> {
const [result, actions] = (createResource as any)(...args);
const wrappedResult = new Proxy(result, {
apply(target, thisArg, argArray) {
try {
return Reflect.apply(target, thisArg, argArray);
} catch {
// get initialValue
// let fallbackValue: T | undefined = undefined;
// const lastArg = args[args.length - 1];
// if (lastArg && typeof lastArg === 'object' && 'initialValue' in lastArg) {
// fallbackValue = (lastArg as any).initialValue;
// }
// return fallbackValue;
return undefined;
}
},
get(target, prop, receiver) {
return Reflect.get(target, prop, receiver);
},
});
return [wrappedResult, actions];
}