State

状态管理是任何应用程序的重要部分。在 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 属性将导致组件自动更新。例如,在上面的示例中,当在按钮点击处理程序中更改属性时。

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 中,有两种创建计算值的方式,每种方式都有不同的用例(按优先顺序):

  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$() 的异步版本,它在值之上包括资源的状态(加载中、已解析、已拒绝)。

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 将不会显示加载指示器。

高级示例

这是一个更完整的示例,使用 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 的调用。这将根据用户输入的名称猜测一个人的年龄,并在用户输入名称时更新。

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 的一个很好的特性是状态可以传递给其他组件。然后只有读取存储的组件才会重新渲染。

有两种将状态传递给其他组件的方式:

  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';
 
// 声明一个上下文 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>;
});

Contributors

Thanks to all the contributors who have helped make this documentation better!

  • 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