状态
状态管理是任何应用程序的重要组成部分。在 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
属性将导致组件自动更新。例如,当在上面的示例中,该属性在按钮点击处理程序中被更改时。
注意 如果您只需要读取信号的值,不要将整个信号作为道具传递,而是只传递它的值。
⛔ 避免:
const isClosedSig = useSignal(false);
return <Child isClosed={isClosedSig} />;
✅ 改为:
const isClosedSig = useSignal(false);
return <Child isClosed={isClosedSig.value} />;
useStore()
与 useSignal()
非常相似,但它接受一个对象作为其初始值,并且反应式性默认扩展到嵌套对象和数组。可以将存储视为一个多值信号或由多个信号组成的对象。
使用 useStore(initialStateObject)
钩子创建一个反应式对象。它接受一个初始对象(或一个工厂函数)并返回一个反应式对象。
import { component$, useStore } from '@builder.io/qwik';
export default component$(() => {
const state = useStore({ count: 0, name: 'Qwik' });
return (
<>
<button onClick$={() => state.count++}>Increment</button>
<p>Count: {state.count}</p>
<input
value={state.name}
onInput$={(_, el) => (state.name = el.value)}
/>
</>
);
});
注意 为了使反应式性按预期工作,请确保保留对反应式对象的引用,而不仅仅是对其属性的引用。例如,执行
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$={() => {
// Even though we are mutating a nested object, this will trigger a re-render
store.nested.fields.are = 'tracked';
}}
>
Clicking me works because store is deep watched
</button>
<br />
<button
onClick$={() => {
// Because store is deep watched, this will trigger a re-render
store.list.push(`Item ${store.list.length}`);
}}
>
Add to list
</button>
<ul>
{store.list.map((item, key) => (
<li key={key}>{item}</li>
))}
</ul>
</>
);
});
请注意,为了使 useStore()
跟踪所有嵌套属性,它需要分配大量 Proxy 对象。如果您有大量嵌套属性,这可能会导致性能问题。在这种情况下,您可以使用 deep: false
选项来仅跟踪顶层属性。
const shallowStore = useStore(
{
nested: {
fields: { are: 'also tracked' }
},
list: ['Item 1'],
},
{ deep: false }
);
处理动态对象修改
当动态操作对象属性时,例如在应用程序中的某个地方渲染它们时删除它们,您可能会遇到问题。如果组件渲染依赖于当前正在删除的对象属性的值,则可能会发生这种情况。为了防止这种情况,在访问属性时使用可选链。例如,如果尝试删除一个属性
delete store.propertyName;
请确保在组件中谨慎地访问此属性,使用可选链 ( ?. )
const propertyValue = store.propertyName?.value;
方法
要在存储中提供方法,您必须将它们转换为 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>
</>
);
});
您知道为什么应该在
useStore()
中使用常规的function(){}
而不是箭头函数吗?这是因为 箭头函数在 javascript 中没有自己的this
绑定。这意味着,如果您尝试使用箭头函数访问this
,this.count
可能指向另一个对象的count
😱。
计算状态
在 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$()
的异步版本,除了值之外还包括资源的 state
(加载、已解析、已拒绝)。
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 }) => {
// it will run first on mount (server), then re-run whenever prNumber changes (client)
// this means this code will run on the server and the browser
track(() => prNumber.value);
const response = await fetch(
`https://api.github.com/repos/QwikDev/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 请求中作为初始 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 执行 fetch 调用。这将根据用户键入的姓名猜测一个人的年龄,并在用户在姓名输入框中键入时更新。
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>
Enter your name, and I'll guess your age!
<input onInput$={(ev, el) => (name.value = el.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';
// Declare a context ID
export const CTX = createContextId<{ count: number }>('stuff');
export default component$(() => {
const userData = useStore({ count: 0 });
// Provide the store to the context under the context 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 is not serializable, so we can't serialize it as part of SSR
// We can however instantiate it on the client after the component is visible
store.monacoInstance = noSerialize(editor);
});
return <div ref={editorRef}>loading...</div>;
});