最近在写一个 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")!);

好的,我们能看到正常情况和数据为空的情况下一切安好。

SolidJS 的反直觉 createResource API 设计-天真的小窝
SolidJS 的反直觉 createResource API 设计-天真的小窝

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

SolidJS 的反直觉 createResource API 设计-天真的小窝

于是乎我理所当然的就想打印一下看看 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 的反直觉 createResource API 设计-天真的小窝

然后我开始怀疑自我了,难道说是 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];
}