狀態

狀態管理是任何應用程式的重要部分。在 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 屬性會導致元件自動更新。 例如,當屬性在上面的範例中,於按鈕點擊處理常式中被變更時。

注意 如果您只需要讀取訊號的值,請不要將整個訊號作為 prop 傳遞,而應僅傳遞其值。

避免:

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 請求的一部分開始獲取數據。作為服務器端渲染 (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> 對象,它的工作方式類似於反應式承諾,包含數據和資源狀態。

狀態 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>
          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 的一個優點是可以將狀態傳遞給其他組件。寫入 store 只會重新渲染從 store 讀取數據的組件。

有兩種方法可以將狀態傳遞給其他組件

  1. 使用 props 將狀態顯式傳遞給子組件,
  2. 或通過 context 隱式傳遞狀態。

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

使用 context

context API 是一種在不通過 props 傳遞狀態的情況下將狀態傳遞給組件的方法(即:避免 props drilling 問題)。樹中的所有後代組件都可以自動訪問對狀態的引用,並具有讀/寫訪問權限。

有關更多信息,請查看 context 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