狀態
狀態管理是任何應用程式的重要部分。在 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
屬性會導致元件自動更新。 例如,當屬性在上面的範例中,於按鈕點擊處理常式中被變更時。
注意 如果您只需要讀取訊號的值,請不要將整個訊號作為 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
綁定。 這表示如果您嘗試使用箭頭函式存取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 請求的一部分開始獲取數據。作為服務器端渲染 (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>
對象,它的工作方式類似於反應式承諾,包含數據和資源狀態。
狀態 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 讀取數據的組件。
有兩種方法可以將狀態傳遞給其他組件
- 使用 props 將狀態顯式傳遞給子組件,
- 或通過 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>;
});