状态

状态管理是任何应用程序的重要组成部分。在 Qwik 中,有两种类型的状态:反应式状态和静态状态。

  1. 静态状态是指任何可以被序列化的内容:字符串、数字、对象、数组……任何东西。
  2. 另一方面,反应式状态是使用 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 绑定。这意味着,如果您尝试使用箭头函数访问 thisthis.count 可能指向另一个对象的 count 😱。

计算状态

在 Qwik 中,有两种方法可以创建计算值,每种方法都有不同的用例(按优先级排序)

  1. useComputed$()useComputed$() 是创建计算值的推荐方法。当计算值可以从源状态(当前应用程序状态)同步推导出来时使用它。例如,创建字符串的小写版本或将姓氏和名字组合成全名。

  2. 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 不会使用加载指示器进行渲染。

高级示例

使用 AbortControllertrackcleanup 获取数据的更完整示例。此示例将根据用户键入的查询获取笑话列表,自动对查询中的更改做出反应,包括中止当前挂起的请求。

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 的一项很酷的功能是,可以将状态传递给其他组件。然后,写入存储只会重新渲染从存储中读取的组件。

有两种方法可以将状态传递给其他组件

  1. 使用 props 显式地将状态传递给子组件,
  2. 或通过上下文隐式地传递状态。

使用 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>;
});

贡献者

感谢所有帮助改进此文档的贡献者!

  • nnelgxorz
  • the-r3aper7
  • voluntadpear
  • kawamataryo
  • JaymanW
  • RATIU5
  • manucorporat
  • literalpie
  • fum4
  • cunzaizhuyi
  • zanettin
  • ChristianAnagnostou
  • shairez
  • forresst
  • almilo
  • Craiqser
  • XiaoChengyin
  • gkatsanos
  • adamdbradley
  • mhevery
  • wtlin1228
  • AnthonyPAlicea
  • sreeisalso
  • wmertens
  • nicvazquez
  • mrhoodz
  • eecopa
  • fabian-hiller
  • julianobrasil
  • aivarsliepa
  • Balastrong
  • Jemsco
  • shairez