State
状态管理是任何应用程序的重要部分。在 Qwik 中,我们可以区分两种类型的状态,即响应式状态和静态状态:
- 静态状态是可以序列化的任何内容:字符串、数字、对象、数组... 任何东西。
- 另一方面,响应式状态是使用
useSignal()
或useStore()
创建的。
需要注意的是,在 Qwik 中的状态不一定是本地组件状态,而是可以由任何组件实例化的应用程序状态。
useSignal()
使用 useSignal()
来创建一个响应式信号(一种状态形式)。useSignal()
接受一个初始值并返回一个响应式信号。
useSignal()
返回的响应式信号由一个带有单个属性 .value
的对象组成。如果更改信号的 value
属性,任何依赖于它的组件都将自动更新。
import { component$, useSignal } from '@builder.io/qwik';
export default component$(() => {
const count = useSignal(0);
return (
<button onClick$={() => count.value++}>
Increment {count.value}
</button>
);
});
上面的示例显示了如何在计数器组件中使用 useSignal()
来跟踪计数。修改 count.value
属性将导致组件自动更新。例如,在上面的示例中,当在按钮点击处理程序中更改属性时。
useStore()
与 useSignal()
非常相似,但它以对象作为初始值,并且默认情况下,其响应性扩展到嵌套对象和数组。可以将存储视为多值信号或由多个信号组成的对象。
使用 useStore(initialStateObject)
钩子来创建一个响应式对象。它接受一个初始对象(或工厂函数)并返回一个响应式对象。
import { component$, useStore } from '@builder.io/qwik';
export default component$(() => {
const state = useStore({ count: 0 });
return (
<>
<button onClick$={() => state.count++}>Increment</button>
<p>Count: {state.count}</p>
</>
);
});
注意 为了使响应性按预期工作,请确保保留对响应式对象的引用,而不仅仅是其属性。例如,执行
let { count } = useStore({ count: 0 })
,然后对count
进行突变不会触发依赖于该属性的组件的更新。
因为 useStore()
跟踪深层响应性,这意味着存储中的数组和对象也将是响应式的。
import { component$, useStore } from '@builder.io/qwik';
export default component$(() => {
const store = useStore({
nested: {
fields: { are: 'also tracked' },
},
list: ['Item 1'],
});
return (
<>
<p>{store.nested.fields.are}</p>
<button
onClick$={() => {
// 即使我们正在突变嵌套对象,这将触发重新渲染
store.nested.fields.are = 'tracked';
}}
>
点我有效,因为存储是深度监视的
</button>
<br />
<button
onClick$={() => {
// 因为存储是深度监视的,这将触发重新渲染
store.list.push(`Item ${store.list.length}`);
}}
>
添加到列表
</button>
<ul>
{store.list.map((item, index) => (
<li key={`items-${index}`}>{item}</li>
))}
</ul>
</>
);
});
请注意,为了跟踪所有嵌套属性,useStore()
需要分配大量的代理对象。如果有很多嵌套属性,这可能会导致性能问题。在这种情况下,您可以使用 deep: false
选项,仅跟踪顶级属性。
const shallowStore = useStore(
{
nested: {
fields: { are: 'also tracked' }
},
list: ['Item 1'],
},
{ deep: false }
);
方法
要在存储上提供方法,必须将它们转换为 QRL,并使用 this
引用存储,如下所示:
import { component$, useStore, $, type QRL } from "@builder.io/qwik";
type CountStore = { count: number; increment: QRL<(this: CountStore) => void> };
export default component$(() => {
const state = useStore<CountStore>({
count: 0,
increment: $(function (this: CountStore) {
this.count++;
}),
});
return (
<>
<button onClick$={() => state.increment()}>Increment</button>
<p>Count: {state.count}</p>
</>
);
});
计算状态
在 Qwik 中,有两种创建计算值的方式,每种方式都有不同的用例(按优先顺序):
-
useComputed$()
:useComputed$()
是创建计算值的首选方式。当计算值可以纯粹地从源状态(当前应用程序状态)同步派生时,请使用它。例如,创建字符串的小写版本或将名字和姓氏组合成全名。 -
useResource$()
: 当计算值是异步的或状态来自应用程序外部时,请使用useResource$()
。例如,基于当前位置(应用程序内部状态)获取当前天气(外部状态)。
除了上述两种创建计算值的方式,还有一种更低级的方式(useTask$()
)。这种方式不会产生新的信号,而是修改现有状态或产生副作用。
useComputed$()
使用 useComputed$
允许对从其他状态同步派生的值进行记忆。
它类似于其他框架中的 memo
,因为它只会在输入信号之一发生更改时重新计算值。
import { component$, useComputed$, useSignal } from '@builder.io/qwik';
export default component$(() => {
const name = useSignal('Qwik');
const capitalizedName = useComputed$(() => {
// it will automatically reexecute when name.value changes
return name.value.toUpperCase();
});
return (
<>
<input type="text" bind:value={name} />
<p>Name: {name.value}</p>
<p>Capitalized name: {capitalizedName.value}</p>
</>
);
});
注意 因为
useComputed$()
是同步的,所以不需要显式跟踪输入信号。
useResource$()
使用 useResource$()
创建异步派生的计算值。这是 useComputed$()
的异步版本,它在值之上包括资源的状态(加载中、已解析、已拒绝)。
useResource$()
的常见用例是在组件内部从外部 API 获取数据,这意味着执行可能发生在服务器端或客户端。
useResource$
钩子应与 <Resource />
一起使用。<Resource />
组件是根据资源状态方便地渲染不同 UI 的一种方式。
import {
component$,
Resource,
useResource$,
useSignal,
} from '@builder.io/qwik';
export default component$(() => {
const prNumber = useSignal('3576');
const prTitle = useResource$<string>(async ({ track }) => {
// 它将首先在挂载时运行(服务器端),然后在 prNumber 更改时重新运行(客户端)
// 这意味着此代码将在服务器端和浏览器端运行
track(() => prNumber.value);
const response = await fetch(
`https://api.github.com/repos/BuilderIO/qwik/pulls/${prNumber.value}`
);
const data = await response.json();
return data.title as string;
});
return (
<>
<input type="number" bind:value={prNumber} />
<h1>PR#{prNumber}:</h1>
<Resource
value={prTitle}
onPending={() => <p>Loading...</p>}
onResolved={(title) => <h2>{title}</h2>}
/>
</>
);
});
注意: 关于
useResource$
的重要一点是它在初始组件渲染时执行(就像useTask$()
回调一样)。通常情况下,希望在组件渲染之前作为初始 HTTP 请求的一部分在服务器端开始获取数据。作为 SSR 的一部分获取数据是加载数据的更常见和首选方式,可以通过routeLoader$
API 完成。useResource$
更多地是一种在浏览器中获取数据时有用的低级 API。在许多方面,
useResource$
与useTask$
类似。它们的主要区别在于:
useResource$
允许返回一个“值”。useResource$
在资源正在解析时不会阻止渲染。有关在初始 HTTP 请求的早期获取数据的详细信息,请参阅
routeLoader$
。
注意: 在 SSR 过程中,
<Resource>
组件将暂停渲染,直到资源解析完成。这样 SSR 将不会显示加载指示器。
高级示例
这是一个更完整的示例,使用 AbortController
、track
和 cleanup
来获取数据。该示例将根据用户输入的查询获取笑话列表,并自动响应查询的更改,包括中止当前挂起的请求。
import {
component$,
useResource$,
Resource,
useSignal,
} from '@builder.io/qwik';
export default component$(() => {
const query = useSignal('busy');
const jokes = useResource$<{ value: string }[]>(
async ({ track, cleanup }) => {
track(() => query.value);
// A good practice is to use `AbortController` to abort the fetching of data if
// new request comes in. We create a new `AbortController` and register a `cleanup`
// function which is called when this function re-runs.
const controller = new AbortController();
cleanup(() => controller.abort());
if (query.value.length < 3) {
return [];
}
const url = new URL('https://api.chucknorris.io/jokes/search');
url.searchParams.set('query', query.value);
const resp = await fetch(url, { signal: controller.signal });
const json = (await resp.json()) as { result: { value: string }[] };
return json.result;
}
);
return (
<>
<label>
Query: <input bind:value={query} />
</label>
<button>search</button>
<Resource
value={jokes}
onPending={() => <>loading...</>}
onResolved={(jokes) => (
<ul>
{jokes.map((joke, i) => (
<li key={i}>{joke.value}</li>
))}
</ul>
)}
/>
</>
);
});
正如上面的示例所示,useResource$()
返回一个 ResourceReturn<T>
对象,它的工作方式类似于一个响应式的 Promise,包含数据和资源状态。
状态 resource.loading
可以是以下之一:
false
- 数据尚不可用。true
- 数据可用(已解析或已拒绝)。
传递给 useResource$()
的回调会在 useTask$()
回调完成后立即运行。有关更多详细信息,请参阅 生命周期 部分。
<Resource />
<Resource />
是用于与 useResource$()
一起使用的组件,根据资源的状态渲染不同的内容。
<Resource
value={weatherResource}
onPending={() => <div>Loading...</div>}
onRejected={() => <div>Failed to load weather</div>}
onResolved={(weather) => {
return <div>Temperature: {weather.temp}</div>;
}}
/>
值得注意的是,使用 useResource$()
时并不需要 <Resource />
。它只是一种方便地渲染资源状态的方式。
这个示例展示了如何使用 useResource$
来执行对 agify.io API 的调用。这将根据用户输入的名称猜测一个人的年龄,并在用户输入名称时更新。
import {
component$,
useSignal,
useResource$,
Resource,
} from '@builder.io/qwik';
export default component$(() => {
const name = useSignal<string>();
const ageResource = useResource$<{
name: string;
age: number;
count: number;
}>(async ({ track, cleanup }) => {
track(() => name.value);
const abortController = new AbortController();
cleanup(() => abortController.abort('cleanup'));
const res = await fetch(`https://api.agify.io?name=${name.value}`, {
signal: abortController.signal,
});
return res.json();
});
return (
<section>
<div>
<label>
输入您的姓名,我会猜测您的年龄!
<input
onInput$={(e: Event) =>
(name.value = (e.target as HTMLInputElement).value)
}
/>
</label>
</div>
<Resource
value={ageResource}
onPending={() => <p>Loading...</p>}
onRejected={() => <p>Failed to person data</p>}
onResolved={(ageGuess) => {
return (
<p>
{name.value && (
<>
{ageGuess.name} {ageGuess.age} years
</>
)}
</p>
);
}}
/>
</section>
);
});
传递状态
Qwik 的一个很好的特性是状态可以传递给其他组件。然后只有读取存储的组件才会重新渲染。
有两种将状态传递给其他组件的方式:
- 通过显式使用 props 将状态传递给子组件,
- 或通过上下文隐式传递状态。
使用 props
将状态传递给其他组件的最简单方式是通过 props 传递。
import { component$, useStore } from '@builder.io/qwik';
export default component$(() => {
const userData = useStore({ count: 0 });
return <Child userData={userData} />;
});
interface ChildProps {
userData: { count: number };
}
export const Child = component$<ChildProps>(({ userData }) => {
return (
<>
<button onClick$={() => userData.count++}>Increment</button>
<p>Count: {userData.count}</p>
</>
);
});
使用上下文
上下文 API 是一种在不必通过 props 传递状态的情况下将状态传递给组件的方式(即:避免了 prop 传递问题)。自动地,树中所有后代组件都可以访问对状态的引用,并对其进行读写访问。
有关更多信息,请查看 上下文 API。
import {
component$,
createContextId,
useContext,
useContextProvider,
useStore,
} from '@builder.io/qwik';
// 声明一个上下文 ID
export const CTX = createContextId<{ count: number }>('stuff');
export default component$(() => {
const userData = useStore({ count: 0 });
// 使用上下文 ID 为上下文提供存储
useContextProvider(CTX, userData);
return <Child />;
});
export const Child = component$(() => {
const userData = useContext(CTX);
return (
<>
<button onClick$={() => userData.count++}>Increment</button>
<p>Count: {userData.count}</p>
</>
);
});
noSerialize()
Qwik 确保所有应用程序状态始终是可序列化的。这对于确保 Qwik 应用具有 可恢复性 属性非常重要。
有时需要存储无法序列化的数据,noSerialize()
告诉 Qwik 甚至不要尝试序列化标记的值。
例如,对于第三方库的引用,比如 Monaco 编辑器,将始终需要 noSerialize()
,因为它是不可序列化的。
如果将值标记为不可序列化,那么该值将不会在序列化事件(例如从服务器端 SSR 恢复应用程序时)中保留。在这种情况下,该值将被设置为 undefined
,开发人员需要在客户端重新初始化该值。
import {
component$,
useStore,
useSignal,
noSerialize,
useVisibleTask$,
type NoSerialize,
} from '@builder.io/qwik';
import type Monaco from './monaco';
import { monacoEditor } from './monaco';
export default component$(() => {
const editorRef = useSignal<HTMLElement>();
const store = useStore<{ monacoInstance: NoSerialize<Monaco> }>({
monacoInstance: undefined,
});
useVisibleTask$(() => {
const editor = monacoEditor.create(editorRef.value!, {
value: 'Hello, world!',
});
// Monaco 不可序列化,因此我们无法将其作为 SSR 的一部分序列化
// 但是我们可以在组件可见后在客户端实例化它
store.monacoInstance = noSerialize(editor);
});
return <div ref={editorRef}>loading...</div>;
});