التوثيق
التفاعلية
يحدّث NSH واجهة المستخدم عبر الإشارات (signals): قيم تعرف من يقرؤها. عندما تتغير إشارة، يعيد NSH تشغيل الحسابات التي قرأتها بالضبط (نص، أو نمط، أو تأثير) ولا شيء غير ذلك. لا تُعاد معالجة المكونات. النموذج مأخوذ من Solid، أما الرسم التفاعلي نفسه فهو أصلي (native): تعمل مكونات Rust وTSX على الرسم نفسه المكتوب بلغة Rust، وهكذا يجري الجدولة وتتبع الاعتماديات والتنظيف خارج المفسّر حتى في TSX.
import { batch, cleanup, computed, context, effect, mount, Scroll, signal, Text, untracked, View } from "nsh";
import { button, buttonText, focusRing, tint, ui, wide } from "./palette";
/** The accent's palette key: a key, not a colour, so it follows the theme. */
type Swatch = "accent" | "info" | "up" | "violet";
const Accent = context(signal<Swatch>("accent"));
/** A button filled with a palette colour instead of a surface. */
const filled = (color: () => string) => () => ({ backgroundImage: "none", backgroundColor: color(), borderColor: "rgba(0,0,0,0)", boxShadow: "none" });
/** A stopwatch: the effect starts a timer and its cleanup stops it. */
function Stopwatch() {
const running = signal(false);
const tenths = signal(0);
effect(() => {
if (!running()) return;
const id = setInterval(() => tenths.update((t) => t + 1), 100);
cleanup(() => clearInterval(id));
});
const accent = Accent();
return (
<View style={{ flexDirection: "row", alignItems: "center", gap: 12 }}>
<Text style={{ flex: 1, fontSize: 28, fontWeight: "600", fontVariant: ["tabular-nums"], color: ui().text }}>
{(tenths() / 10).toFixed(1)} s
</Text>
<View onPress={() => running.set(!running())} {...button("primary", { style: filled(() => ui()[accent()]) })}>
<Text style={{ ...buttonText(), color: ui().page }}>{running() ? "Stop" : "Start"}</Text>
</View>
<View onPress={() => tenths.set(0)} {...button()}>
<Text style={buttonText()}>Reset</Text>
</View>
</View>
);
}
function Section(props: { title: string; children: any }) {
return (
<View style={{ ...ui().card, borderRadius: 16, padding: 14, gap: 10 }}>
<Text style={{ fontSize: 13, fontWeight: "600", color: ui().muted }}>{props.title}</Text>
{props.children}
</View>
);
}
function App() {
const price = signal(12);
const quantity = signal(1);
const total = computed(() => price() * quantity());
const writes = signal(0);
const accent = Accent();
// Runs again whenever `total` changes; reads of `writes` are not tracked.
const log = signal<string[]>([]);
effect(() => {
const t = total();
const n = untracked(writes);
log.update((l) => [`total ${t} after ${n} writes`, ...l].slice(0, 3));
});
const step = (dp: number, dq: number) => {
batch(() => {
price.update((p) => Math.max(0, p + dp));
quantity.update((q) => Math.max(0, q + dq));
writes.update((w) => w + 2);
});
};
const swatches: Swatch[] = ["accent", "info", "up", "violet"];
return (
<Scroll style={{ flex: 1, backgroundColor: ui().page }}>
<View
style={
wide()
? { padding: 20, paddingTop: 24, gap: 16, display: "grid", gridTemplateColumns: "repeat(2, 1fr)" }
: { padding: 20, paddingTop: 24, gap: 16 }
}
>
<Section title="signal + computed">
<Text style={{ fontSize: 17, color: ui().text }}>
{price()} × {quantity()} = <Text style={{ fontWeight: "700", color: ui()[accent()] }}>{total()}</Text>
</Text>
<View style={{ flexDirection: "row", gap: 8 }}>
<View onPress={() => step(1, 0)} {...button()}>
<Text style={buttonText()}>price +1</Text>
</View>
<View onPress={() => step(0, 1)} {...button()}>
<Text style={buttonText()}>qty +1</Text>
</View>
<View
onPress={() => step(1, 1)}
{...button("secondary", { style: () => ({ backgroundImage: "none", backgroundColor: tint(ui().warn, 0.14), borderColor: tint(ui().warn, 0.3), boxShadow: "none" }) })}
>
<Text style={{ ...buttonText(), color: ui().warn }}>both (batch)</Text>
</View>
</View>
</Section>
<Section title="effect + untracked">
<Each of={log()}>{(line) => <Text style={{ fontSize: 13, color: ui().muted }}>{line()}</Text>}</Each>
</Section>
<Section title="effect + cleanup">
<Stopwatch />
</Section>
<Section title="context (the value is a signal)">
<View style={{ flexDirection: "row", gap: 10 }}>
<Each of={swatches}>
{(c) => (
<View
onPress={() => accent.set(c())}
style={{ width: 36, height: 36, borderRadius: 18, backgroundColor: ui()[c()], borderWidth: 3, borderColor: accent() === c() ? ui().text : "transparent" }}
hoverStyle={{ opacity: 0.88 }}
focusStyle={focusRing()}
activeStyle={{ opacity: 0.7 }}
/>
)}
</Each>
</View>
</Section>
</View>
</Scroll>
);
}
mount(() => (
<Accent.Provider value={signal<Swatch>("accent")}>
<App />
</Accent.Provider>
));تأخذ مكونات Rust كل ما في هذه الصفحة من prelude، بينما يستورده TSX من nsh:
use nsh_core::prelude::*; // signal, computed, effect, resource, batch, untracked, cleanup, provide_context, use_contextimport { batch, cleanup, computed, context, effect, resource, signal, untracked } from "nsh";#الإشارات
signal(value) تنشئ إشارة. اقرأها باستخدام get() في Rust، أو باستدعائها في TSX؛ وset وupdate تكتبانها:
let count = signal(0);
count.get(); // 0, and tracks the read
count.set(5);
count.update(|n| *n += 1); // 6const count = signal(0);
count(); // 0, and tracks the read
count.set(5);
count.update((n) => n + 1); // 6القراءة داخل نطاق تتبّع (ربط، أو computed، أو effect) تشترك ذلك النطاق في الإشارة. والكتابة تُخطر المشتركين إذا تغيرت القيمة: تُقارَن بـ PartialEq في Rust، وبـ === في TSX.
fn counter() -> impl IntoElement {
let count = signal(0);
view()
.on_press(move || count.update(|n| *n += 1))
.child(text(move || format!("Tapped {} times", count.get())))
}function Counter() {
const count = signal(0);
return (
<View onPress={() => count.update((n) => n + 1)}>
<Text>Tapped {count()} times</Text>
</View>
);
}تؤدي النقرة إلى تحديث عقدة النص الوحيدة التي تقرأ count. أما counter() / Counter نفسها فتعمل مرة واحدة. الإشارة في Rust مقبض Copy، لذا تأخذها الإغلاقات (closures) باستخدام move وتستعملها بحرية.
يمكن للإشارة أن تعدّ كل كتابة تغييرًا، ويمكن لـ TSX مقارنة القيم بطريقته الخاصة:
let tick = signal_with(0, SignalEquals::Never); // every write notifiesconst tick = signal(0, { equals: false }); // every write notifies
const position = signal({ x: 0, y: 0 }, { equals: (a, b) => a.x === b.x && a.y === b.y });في TSX، تبدأ signal<T>() بلا قيمة بالقيمة undefined ويكون نوعها Signal<T | undefined>؛ وفي Rust، يقول Option الشيء نفسه (signal(None::<User>)).
#الكائنات والمصفوفات
في Rust، تغيّر update القيمة في مكانها وتُخطر القرّاء؛ بينما تستبدلها set. في TSX، تُقارَن الإشارات بـ ===، لذا فالقيمة التي تُعدَّل في مكانها لا تُعدّ تغييرًا: اكتب كائنًا أو مصفوفة جديدة.
let todos = signal(Vec::<Todo>::new());
todos.update(|list| list.push(Todo { id: 3, title: "New".into(), done: false })); // notifies
todos.update(|list| {
if let Some(todo) = list.iter_mut().find(|t| t.id == id) {
todo.done = true;
}
});const todos = signal<Todo[]>([]);
todos.update((list) => [...list, { id: 3, title: "New" }]); // notifies
todos.update((list) => list.map((t) => (t.id === id ? { ...t, done: true } : t)));
todos().push(item); // does not notify anyone
todos.set(todos()); // still the same array: no changeeach / <Each> تفهرس الصفوف بالعنصر، فتغيير عنصر واحد يعيد بناء صفه فقط (JSX والمكونات).
#computed
computed(fn) تشتق قيمة من إشارات أخرى. تشغّل fn وتحتفظ بالنتيجة وتعيد تشغيلها عند تغير إشارة قرأتها. قراءتها كقراءة إشارة:
let price = signal(12);
let quantity = signal(2);
let total = computed(move || price.get() * quantity.get());
text(move || format!("Total: {}", total.get()))const price = signal(12);
const quantity = signal(2);
const total = computed(() => price() * quantity());
<Text>Total: {total()}</Text>استخدم computed عندما تُقرأ القيمة المشتقة في عدة أماكن أو تكون مكلفة: فهي تعمل مرة واحدة لكل تغيير مهما كان عدد قرّائها، ولا يُخطَر القرّاء إلا عندما تتغير نتيجتها. أما للقيمة الرخيصة التي تُقرأ مرة واحدة فيكفي إغلاق عادي:
let label = move || format!("{} items", quantity.get());
text(label)const label = () => `${quantity()} items`;
<Text>{label()}</Text>في TSX، تستقبل fn القيمة السابقة (undefined في المرة الأولى)، للقيم التي تتراكم:
const max = computed((prev: number | undefined) => Math.max(prev ?? 0, speed()));
#effect
effect(fn) تشغّل fn، وتعيد تشغيلها كلما تغيرت إشارة قرأتها. التأثيرات مخصصة للآثار الجانبية: التسجيل (logging)، والمؤقتات، وطلبات الشبكة، والاستدعاءات الأمرية على المراجع.
#[derive(Debug, Clone, PartialEq, Props)]
pub struct SearchProps {
/// What to search for.
pub query: Prop<SharedString>,
}
#[component]
pub fn Search(props: SearchProps) -> impl IntoElement {
let results = signal(Vec::<SharedString>::new());
effect(move || {
let q = props.query.get(); // tracked: runs again when the prop changes
if q.is_empty() {
return;
}
spawn(async move {
if let Ok(response) = fetch(format!("https://api.example.com/search?q={q}")).await {
results.set(response.text().lines().map(|line| SharedString::from(line.to_owned())).collect());
}
});
});
view().child(each(results, |item| item.clone(), |item, _| text(item)))
}function Search(props: { query: string }) {
const results = signal<string[]>([]);
effect(() => {
const q = props.query; // tracked: runs again when the prop changes
if (!q) return;
fetch(`https://api.example.com/search?q=${encodeURIComponent(q)}`)
.then((r) => r.json())
.then((data) => results.set(data.items));
});
return <Each of={results()}>{(item) => <Text>{item()}</Text>}</Each>;
}يتتبع التأثير القراءات التي تحدث أثناء تشغيله المتزامن. أما القراءات داخل مهمة، أو دالة .then أو بعد await فلا تُتتبَّع.
التأثير الذي يستدعي مرجعًا يعمل على العنصر الذي عليه المرجع؛ وفي TSX تعمل التأثيرات بعد وجود العناصر المنشأة في الدورة نفسها، فتكون المراجع قد ضُبطت:
let input = element_ref();
effect(move || {
if editing.get() {
input.focus();
}
});let input: InputRef | undefined;
effect(() => {
if (editing()) input?.focus();
});#resource
resource(source, fetcher) تحمّل قيمة بشكل غير متزامن وتحتفظ بحالة التحميل، فيعرض المكوّن البيانات أو مؤشر تحميل أو خطأ دون أن يديرها بنفسه:
let url = signal(SharedString::from("https://api.example.com/quote"));
let quote = resource(move || Some(url.get()), |url, _info| async move {
let response = fetch(url.to_string()).await?;
if !response.ok() {
return Err(format!("HTTP {}", response.status()));
}
Ok(response.text())
});
view()
.child(text(move || match (quote.get(), quote.error()) {
(_, Some(error)) => format!("Failed: {error}"),
(Some(quote), None) => quote,
(None, None) => "Loading…".to_owned(),
}))
.child(text("Reload").on_press(move || quote.refetch()))import { List, resource, signal, Text } from "nsh";
type Post = { id: number; title: string };
const url = signal("https://jsonplaceholder.typicode.com/posts?_limit=20");
const posts = resource(url, async (u, { signal }) => {
const response = await fetch(u, { signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return (await response.json()) as Post[];
});
<If when={posts.error()}>
<Text>Failed: {String(posts.error())}</Text>
</If>
<List
each={posts() ?? []}
refreshing={posts.state() === "refreshing"}
onRefresh={posts.refetch}
>
{(post) => <Text>{post().title}</Text>}
</List>source متتبَّع: كلما أعطى قيمة جديدة، يحمّل fetcher(value, info) من جديد. في Rust هو إغلاق يعيد Option، وNone لا تحمّل شيئًا؛ وفي TSX هو دالة وصول (إشارة، دالة) أو قيمة، وundefined أو null أو false لا تحمّل شيئًا: لمستخدم لم يسجّل الدخول بعد أو لم يُختَر شيء. وعند غياب المصدر، يمرر Rust القيمة || Some(())، ويحمّل resource(fetcher) في TSX مرة واحدة عبر استدعاء fetcher(info). تعيد دالة الجلب في Rust Result؛ أما في TSX فتعيد قيمة أو وعدًا (promise)، ويُعدّ الرمي (throw) فشلًا.
| Rust | TSX | ما هو |
|---|---|---|
quote.get() | posts() | أحدث قيمة: لا شيء قبل انتهاء أول تحميل، وتبقى أثناء إعادة التحميل وبعد الفشل. |
quote.loading() | posts.loading() | هل يجري تحميل. |
quote.error() | posts.error() | خطأ آخر تحميل، ولا شيء بعد أن يعطي تحميل قيمة. |
quote.state() | posts.state() | Unresolved / "unresolved" (لا شيء للتحميل)، Pending (التحميل الأول)، Ready، Refreshing (إعادة التحميل مع بقاء القيمة)، Errored. |
quote.refetch() | posts.refetch() | يحمّل من جديد من المصدر نفسه: إعادة محاولة، أو السحب للتحديث. |
quote.mutate(value) | posts.mutate(value) | يضبط القيمة هنا دون تحميل، وهو تحديث متفائل (optimistic)، ويمسح الخطأ. |
المورد (resource) في Rust مقبض Copy كالإشارات؛ وأعضاء TSX مربوطة، لذا تعمل onRefresh={posts.refetch} وconst loading = posts.loading كما هي.
يُعتدّ بآخر تحميل فقط. التحميل الجديد يُسقط الجاري، وكذلك زوال المكوّن؛ وفي TSX، تُلغي info.signal الـ fetch الممرَّر إليها. التحميل الذي ينتهي بعد أن حلّ محله غيره لا يغيّر شيئًا. info.refetching تدل على أن refetch() بدأ التحميل. resource_with_value(value, source, fetcher) في Rust و{ initialValue } في TSX تمنحان المورد قيمة منذ البداية.
#cleanup
cleanup(fn) تسجّل fn لتعمل قبل إعادة تشغيل التأثير الحالي، أو عند زوال المكوّن أو التأثير الذي تنتمي إليه:
effect(move || {
if !running.get() {
return;
}
let ticker = spawn(async move {
loop {
timer(Duration::from_millis(100)).await;
tick();
}
});
cleanup(move || ticker.cancel()); // before the next run, or on removal
});function Clock() {
const now = signal(Date.now());
const id = setInterval(() => now.set(Date.now()), 1000);
cleanup(() => clearInterval(id)); // when Clock is removed
return <Text>{new Date(now()).toLocaleTimeString()}</Text>;
}
effect(() => {
if (!running()) return;
const id = setInterval(tick, 100);
cleanup(() => clearInterval(id)); // before the next run, or on removal
});في Rust، تنتهي المهام والمؤقتات والفواصل الزمنية (spawn، set_timeout، set_interval) مع المالك الذي أُنشئت تحته، فلا تحتاج ساعة العرض الخاصة إلى تنظيف:
use std::time::Duration;
use nsh_core::time::unix_ms;
fn clock() -> impl IntoElement {
let now = signal(unix_ms());
set_interval(Duration::from_secs(1), move || now.set(unix_ms())); // ends with the view clock() made
text(move || format!("{:.0}", now.get() / 1000.0))
}
يمكن لتأثير TSX أيضًا أن يعيد دالة التنظيف الخاصة به، كما في React: الدالة التي تعيدها fn تعمل في اللحظات نفسها التي تعمل فيها الدالة التي تمررها إلى cleanup.
effect(() => {
const id = setInterval(tick, 100);
return () => clearInterval(id);
});
يزول المكوّن عندما لا يعود معروضًا: يُخفى فرع when / <If> الخاص به، أو يُزال صف each / <Each> الخاص به، أو تُزال صفحته من المكدس (popped). وكل ما أنشأه (التأثيرات، والقيم المحسوبة، والمهام، وعمليات التنظيف، والمكونات المتداخلة) يُتخلَّص منه معه.
#batch
كل كتابة تُخطر مشتركيها فورًا. ولجعل عدة كتابات تُحسب تغييرًا واحدًا، غلّفها بـ batch: تعمل الاعتماديات مرة واحدة بعد آخر كتابة.
let save = move || {
batch(|| {
first_name.set(form.first.clone());
last_name.set(form.last.clone());
dirty.set(false);
})
};const save = () =>
batch(() => {
firstName.set(form.first);
lastName.set(form.last);
dirty.set(false);
});معالجات الأحداث لا تُجمَّع تلقائيًا: كتابتان في معالج الضغط تشغّلان تأثيرًا يقرأ الإشارتين مرتين. تعيد batch ما تعيده دالتها.
#untracked
untracked(fn) تشغّل fn دون الاشتراك فيما تقرؤه. استخدمها داخل نطاق تتبّع لقراءة قيمة دون الاعتماد عليها:
effect(move || {
let q = query.get(); // runs again when query changes
let filters = untracked(|| active_filters.get()); // read, but not a reason to run again
search(q, filters);
});effect(() => {
const q = query(); // runs again when query changes
const filters = untracked(activeFilters); // read, but not a reason to run again
search(q, filters);
});تقرأ get_untracked() في Rust إشارة واحدة بالطريقة نفسها. جسم المكوّن غير متتبَّع أصلًا: قراءة إشارة هناك تقرؤها مرة واحدة.
#context
السياق (context) قيمة يوفّرها مكوّن لكل ما يعرضه، مهما بلغ عمقه. في Rust، توفّر provide_context(value) قيمة من نوعها، وتجد use_context::<T>() أقرب قيمة؛ وفي TSX، تنشئ context(defaultValue) سياقًا، واستدعاؤه يقرأ أقرب قيمة موفَّرة (أو القيمة الافتراضية)، وتوفّر <Ctx.Provider value> قيمة:
#[derive(Clone, Copy)]
struct Dark(bool);
fn label(content: &'static str) -> impl IntoElement {
let dark = use_context::<Dark>().is_some_and(|d| d.0);
text(content).text_color(if dark { rgb(0xffffff) } else { rgb(0x1f2328) })
}
fn screen() -> impl IntoElement {
provide_context(Dark(true));
view().child(label("Hello"))
}const Theme = context<"light" | "dark">("light");
function Label(props: { children: string }) {
const theme = Theme();
return <Text style={{ color: theme === "dark" ? "#ffffff" : "#1f2328" }}>{props.children}</Text>;
}
mount(() => (
<Theme.Provider value="dark">
<Label>Hello</Label>
</Theme.Provider>
));القيمة الموفَّرة تُقرأ مرة واحدة، عند البحث عنها. ولتوفير شيء يتغير، وفّر إشارة (أو بنية struct من الإشارات) واقرأها في ربط:
#[derive(Clone, Copy)]
struct Accent(Signal<Hsla>);
#[component]
fn App() -> impl IntoElement {
provide_context(Accent(signal(rgb(0x0969da).into())));
view().child(swatch())
}
fn swatch() -> impl IntoElement {
let Accent(accent) = use_context::<Accent>().expect("an accent");
view()
.w(px(40.0))
.h(px(40.0))
.bg(move || accent.get())
.on_press(move || accent.set(rgb(0x8250df).into()))
}const Accent = context(signal("#0969da"));
function App() {
const accent = signal("#0969da");
return (
<Accent.Provider value={accent}>
<Swatch />
</Accent.Provider>
);
}
function Swatch() {
const accent = Accent();
return <View onPress={() => accent.set("#8250df")} style={{ width: 40, height: 40, backgroundColor: accent() }} />;
}اقرأ السياق في جسم المكوّن (أو في شيفرة تعمل أثناء إنشاء المكونات)، لا في معالج حدث أو مؤقت، حيث لا يوجد مكوّن للبحث منه. تأخذ مكونات Rust سمة (theme) التطبيق بهذه الطريقة أيضًا: theme() من nsh_base (السمة).
#Error boundaries
تحصر حدود الأخطاء (error boundary) الفشل في جزء واحد من العرض. عندما يرمي شيء تحتها (في Rust: panic) أثناء العرض أو التشغيل، مثل جسم مكوّن أو ربط أو تأثير أو معالج حدث، يُزال الجزء الفاشل وتعرض الحدود بديلها (fallback) مكانه؛ ويبقى بقية العرض كما هو. في Rust هي catch_error(view, fallback)؛ وفي TSX هي <ErrorBoundary fallback> من nsh. يتلقى البديل الخطأ (في TSX القيمة المرمية نفسها، وهي عادةً Error) ودالة إعادة ضبط (reset) تعرض العرض من جديد:
fn profile_card(user: Signal<Option<User>>) -> impl IntoElement {
catch_error(
move || profile(user),
|error, reset| {
view()
.child(text(format!("Something went wrong: {error}")))
.child(text("Try again").on_press(move || reset.reset()))
},
)
}import { ErrorBoundary, Text, View } from "nsh";
function ProfileCard(props: { user: User | undefined }) {
return (
<ErrorBoundary
fallback={(error, reset) => (
<View>
<Text>Something went wrong: {error instanceof Error ? error.message : String(error)}</Text>
<Text onPress={reset}>Try again</Text>
</View>
)}
>
<Profile user={props.user} />
</ErrorBoundary>
);
}يذهب الفشل إلى أقرب حدود فوق موضع حدوثه، ويذهب الفشل داخل البديل إلى الحدود التي فوقها. وبدون حدود، يُبلَّغ عن الفشل كخطأ غير ملتقَط (طبقة الأخطاء أثناء التطوير، والسجل). في TSX، يُعرض عنصر fallback كما هو (fallback={<Text>Unavailable</Text>})، وتُستدعى الدالة مع الخطأ وreset، مهما كانت المعاملات التي تعلنها.
#ما الذي يعمل، ومتى
| ما تكتبه | يعمل | يعمل من جديد عندما |
|---|---|---|
| جسم المكوّن | مرة واحدة، عند إنشائه | أبدًا (باستثناء إعادة التحميل الساخن) |
ربط: إغلاق يُعطى إلى text أو نمط أو خاصية (prop) في Rust؛ {expr} أو prop={expr} يستدعي أو يقرأ خاصية في TSX | مرة واحدة، عند إنشاء العنصر | تتغير إشارة قرأها؛ تُحدَّث تلك الخاصية أو ذلك الابن فقط |
computed(fn) | عند إنشائه | تتغير إشارة قرأها |
effect(fn) | بعد وجود عناصر الدورة الحالية | تتغير إشارة قرأها |
when(cond, ..) / <If when> | عند إنشائه | يتغير تحقق الشرط |
صف each(..) / <Each of> | عند ظهور مفتاحه أول مرة | أبدًا؛ بل تتغير إشارتا عنصره وفهرسه |
البيئة تفاعلية بالطريقة نفسها: window() وdark() وfont_scale() وsafe_area() وkeyboard_height() في Rust، وwindowSize() وcolorScheme() وfontScale() من nsh وsafeArea() وkeyboardFrame() من nsh في TSX، لذا فالربوط التي تقرؤها تتبع التدوير والوضع الداكن ولوحة المفاتيح.
#أخطاء شائعة
قراءة إشارة في جسم المكوّن وانتظار التحديثات.
fn bad() -> impl IntoElement {
let n = count.get(); // read once
text(format!("{n}")) // never changes
}
fn good() -> impl IntoElement {
text(move || format!("{}", count.get())) // a binding: follows count
}function Bad() {
const n = count(); // read once
return <Text>{n}</Text>; // never changes
}
function Good() {
return <Text>{count()}</Text>;
}قراءة الخصائص مرة واحدة. خاصية Rust المقروءة بـ props.title.get() في الجسم تُقرأ مرة واحدة؛ اقرأها في ربط (text(props.title)، move || props.title.get()). وفي TSX، تقرأ const { title } = props قيمة title مرة واحدة: اقرأ props.title حيث تستخدمها، أو قسّم الخصائص باستخدام split.
التعديل بدل الضبط، في TSX. list().push(x) يغيّر المصفوفة لكنه لا يُخطر أحدًا. اكتب مصفوفة جديدة، كما تفعل update في Rust نيابةً عنك.
اشتقاق الحالة بالتأثيرات. effect(() => total.set(price() * qty())) تعمل، لكنها تترك total قديمة بين الكتابة والتأثير، وتكلّف دورة ثانية. استخدم computed.
إنشاء التأثيرات خارج المكونات. التأثيرات والقيم المحسوبة تنتمي إلى المكوّن الذي أنشأها ويُتخلَّص منها معه. وإذا أُنشئت في معالج حدث أو دالة مؤقت فلا مالك لها وتبقى إلى الأبد. أنشئها في جسم مكوّن ودعها تقرأ الإشارات التي تتغير.
توقع تتبّع الشيفرة غير المتزامنة. تُتتبَّع فقط القراءات التي تحدث بشكل متزامن أثناء تشغيل تأثير أو قيمة محسوبة. اقرأ الإشارات أولًا، ثم ابدأ العمل غير المتزامن.
#ذات صلة
- JSX والمكونات: المكونات والخصائص و
when/<If>وeach/<Each>، في Rust وTSX. - البناء باستخدام Rust: تطبيقات Rust والمهام والسمة.
- مرجع وحدة
nsh: توقيع كل دالة في TSX. - الحركة (Animation): قيم متحركة تتحرك على خيط واجهة المستخدم دون إشارات.