@preact-signals/utils
@preact-signals/utils
is a standard library for Preact Signals, designed to provide essential utilities for comfortable and streamlined usage. This package includes several features to enhance the flexibility and maintainability of Preact Signal-based projects.
Split your code because of you want, not because of you have to
Table of Contents
Prerequisites
Ensure that one of the preact signals runtimes is installed:
@preact/signals
for preact
, requiring an additional step.@preact/signals-core
for vanilla js requiring an additional step.@preact-signals/safe-react
for react
, requiring an additional step.@preact/signals-react
for react
.
@preact/signals-core
additional step:
We need to resolve @preact/signals-react
as @preact/signals-core
Vite example:
import { defineConfig } from "vite";
export default defineConfig({
resolve: {
alias: {
"@preact/signals-react": "@preact/signals-core",
},
},
});
Astro example:
import { defineConfig } from "astro/config";
export default defineConfig({
vite: {
resolve: {
alias: {
"@preact/signals-react": "@preact/signals-core",
},
},
},
});
@preact-signals/safe-react
additional step:
Resolve @preact-signals/safe-react
as @preact/signals-react
Vite example:
import { defineConfig } from "vite";
export default defineConfig({
resolve: {
alias: {
"@preact/signals-react": "@preact-signals/safe-react",
},
},
});
Astro example:
import { defineConfig } from "astro/config";
export default defineConfig({
vite: {
resolve: {
alias: {
"@preact/signals-react": "@preact/signals-core",
},
},
},
});
Next.js
module.exports = {
webpack: (config) => {
config.resolve.alias = {
...config.resolve.alias,
"@preact/signals-react": "@preact-signals/safe-react",
};
return config;
},
};
@preact/signals
additional step:
Resolve @preact/signals-react
as @preact/signals
. For guidance, see resolve react
as preact
and follow a similar process with signals. Additionally, dedupe preact
.
import preact from "@preact/preset-vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [preact()],
resolve: {
dedupe: ["preact"],
alias: [
{ find: "react", replacement: "preact/compat" },
{ find: "react-dom/test-utils", replacement: "preact/test-utils" },
{ find: "react-dom", replacement: "preact/compat" },
{ find: "react/jsx-runtime", replacement: "preact/jsx-runtime" },
{ find: "@preact/signals-react", replacement: "@preact/signals" },
],
},
});
Installation
Fetch @preact-signals/utils
via your preferred package manager:
npm install @preact-signals/utils
yarn add @preact-signals/utils
pnpm add @preact-signals/utils
Library consist from many entries:
@preact-signals/utils
library agnostic utils and deep reactivity implementation@preact-signals/utils/components
for reactive components@preact-signals/utils/hooks
for reactive hooks@preact-signals/utils/hocs
provides hocs wrappers that bring reactivity to your components@preact-signals/utils/macro
provides babel macros
@preact-signals/utils/macro
: Macros. Improving ergonomics
This entry provides macros distributed as babel plugin. It's allows you to write more concise code. There are two types of macros: state macros
, and ref macro shorthand.
You can play with it in the interactive playground
State macros
Shorthands that allows you to do omit .value
access and work with signals like with regular values.
Allowed state macros:
$state
$useState
$useLinkedState
$derived
$useDerived
$deref
Here is example of how it works. You can play with it here
Ref macro shorthand
import { $$ } from "@preact-signals/utils/macro";
const a = signal(1);
const b = signal(2);
const C = () => <div>{$$(a.value + b.value)}</div>;
Macro setup
To use macros you need to add babel plugin to your babel config:
{
"plugins": [
[
"module:@preact-signals/utils/babel",
{
"experimental_stateMacros": true
}
]
]
}
Main Entry: @preact-signals/utils
ReactiveRef
/$
The ReactiveRef
type functions similarly to a Preact signal, essentially wrapping a function that can be passed into props or JSX. You can create it using the $
function.
const sig = signal(1);
<div>{$(() => sig.value * 10)}</div>;
WritableReactiveRef
/$w
Creates editable signal from getter and setter functions.
const a = signal({ a: 1 });
const aField = $w({
get() {
return a.value.a;
},
set(value) {
a.value = { a: value };
},
});
console.log(aField.value);
aField.value = 2;
console.log(aField.value);
console.log(a.value);
ReducerSignal
Reducer pattern for signals
const reducer = (it: number, action: { type: "increment" | "decrement" }) => {
switch (action.type) {
case "increment":
return it + 1;
case "decrement":
return it - 1;
}
};
const counter = reducerSignal(0, reducer);
effect(() => {
console.log("counter value", counter.value);
});
counter.dispatch({ type: "increment" });
const { dispatch } = reducerSignal;
dispatch({ type: "increment" });
Implementation
Deep Reactivity (Port of Vue 3 deep tracking API)
deepSignal
Takes an inner value and returns a reactive and mutable signal, with deepReactive inside of it.
const a = deepSignal({ a: 1, b: 2 });
const c = computed(() => a.value.a + a.value.b);
a.value = { a: 2, b: 3 };
console.log(c.value);
a.value.b = 4;
console.log(c.value);
Store API
Store API contains Vue-like API for deep reactivity. It's has one significant change: if reactive
wrapper is deep - it will be named with deep
prefix. So reacitve
becomes deepReactive
.
Vue | @preact-signals/utils/store |
---|
reactive | Store.deepReactive |
readonly | Store.deepReadonly |
shallowReadonly | Store.shallowReadonly |
shallowReactive | Store.shallowReactive |
isReactive | Store.isReactive |
isReadonly | Store.isReadonly |
isProxy | Store.isProxy |
toRaw | Store.toRaw |
markRaw | Store.markRaw |
To use Store API you should wrap your object with Store.deepReactive
or Store.shallowReactive
:
import { Store } from "@preact-signals/utils";
const a = Store.deepReactive({ a: 1, b: 2 });
const b = Store.shallowReactive({ a: 1, b: 2 });
const c = computed(() => a.value.a + a.value.b + b.value.a + b.value.b);
reaction
The reaction
function allows responding to changes tracked within a dependent function. It is useful for managing side-effects or synchronizing non-reactive parts of your code.
reaction
is enhanced version of this:
const reaction = (deps, fn) =>
effect(() => {
const value = deps();
untracked(() => fn(value));
});
const sig = signal(1);
const sig2 = signal(2);
const dispose = reaction(
() => sig.value,
(value) => {
if (sig2.value * 10 === value) {
sig2.value = value;
}
}
);
reaction(
() => {
sig.value;
return sig2.value;
},
() => {
console.log("reacted");
return () => {
console.log("reaction disposed");
};
},
{
memoize: true,
}
);
rafReaction
Will execute reaction after deps changed on next animation frame. Return dispose function.
const sig = signal(1);
const el = document.createElement("div");
rafReaction(
() => sig.value,
(value) => {
el.style.transform = `translateX(${value}px)`;
}
);
sig.value = 10;
accessor
/setter
These functions act as wrapper creators for signals, offering a convenient way to separate reading and writing responsibilities.
resource
The resource
type binds a signal to a promise, including Preact Signals' reactivity. It can be retried, rejected, or resolved, offering a streamlined way to manage asynchronous operations.
const [resource, { refetch }] = createResource({
fetcher: async () => {
const response = await fetch("https://example.com");
return response.json();
},
});
return <Show when={resource}>{(result) => <div>{result()}</div>}</Show>;
createFlatStore
This function offers a simple store implementation, converting key values into signals on demand.
When deep reactivity API will be stable, possibly flat stores will be deprecated
const [store, setStore] = createFlatStore({
a: 1,
b: 2,
get c() {
return this.a + this.b;
},
});
const c = computed(() => store.a + store.b);
store.a = 2;
store.b = 3;
console.log(store.c);
console.log(c.value);
setStore({ a: 3, b: 4 });
console.log(c.value);
createFlatStoreOfSignals
This function wraps provided signals and value to flat store. You can pass computed's too and it will be readonly field
When deep reactivity API will be stable, possibly flat stores will be deprecated
const [store, setStore] = createFlatStoreOfSignals({
a: 1,
b: 2,
c: signal(10),
d: computed(() => 10),
get e() {
return this.a + this.b;
},
});
setStore({
a: 10,
b: 11,
c: 12,
});
setStore({
e: 10,
d: 10,
});
@preact-signals/utils/hooks
: Hooks for Signals
This entry provides hooks designed to work with signals, enhancing reactivity and composability in your components.
It provides hooks like
const a = useInitSignal(() => new Set());
const b = useComputedOnce(() => a.value.size);
useSignalEffectOnce(() => a.value.size);
const [store, setStore] = useFlatStore(() => ({
a: 1,
b: 2,
}));
const [store2, setStore2] = useFlatStoreOfSignals(() => ({
a: 1,
b: signal(10),
}));
const [resource, { refetch }] = useResource({
fetcher: async () => {
const response = await fetch("https://example.com");
return response.json();
},
});
Deep Reactivity Hooks
Exports hook which takes as first argument callback which will be applied on the first rended to create reactive
primitive.
useDeepSignal
Takes creator callback and returns a reactive and mutable signal, with deepReactive inside of it.
const a = useDeepSignal(() => ({ a: 1, b: 2 }));
There are also: useDeepReactive
, useShallowReactive
.
useLinkedSignal
Creates signal that linked to value passed to hook, with unwrapping of signals to avoid .value.value
const s1 = useLinkedSignal(Math.random() > 0.5 ? 1 : 0);
console.log(s1.peek());
const s2 = useLinkedSignal(Math.random() > 0.5 ? signal(true) : false);
console.log(s2.peek());
const s3 = useLinkedSignal(signal(signal(signal(false))));
console.log(s3.peek());
@preact-signals/utils/components
: Reactive Components
This section includes components like Show
, Switch
, Match
, For
, allowing you to scope reactivity within your JSX. These components aid in writing more declarative and readable code.
All of this component works with reactive unit, which is Signal or Accessor callback
import { Show, For } from "@preact-signals/utils/components";
<Show fallback={<p>Loading...</p>} when={() => arr.value}>
{(data) => (
<ul>
<For each={data} keyExtractor={(item) => item.id}>
{(item) => <li>{renderItem(item)}</li>}
</For>
</ul>
)}
</Show>;
import { Switch, Match } from "@preact-signals/utils/components";
<Switch fallback={<p>Not found</p>}>
<Match when={() => route.value === "home"}>
<Home />
</Match>
<Match when={() => route.value === "about"}>
<About />
</Match>
<Match when={() => route.value === "users"}>
<Users />
</Match>
</Switch>;
@preact-signals/utils/hocs
: High Order Components (HOCs)
HOCs in this entry allow you to inject signals or ReactiveRef
-s into props, aiding in the creation of reusable and composable logic across various components.
Examples:
const View$ = withSignalProps(View);
const Text$ = withSignalProps(Text);
const a = signal(10);
const b = signal(5);
<View$ hitSlop={useComputed(() => a.value + b.value)} />;
<View$ hitSlop={$(() => a.value + b.value)} />;
reactifyLite
Makes you component truly reactive. Your props are will be use getter to signals under the hood (destructuring forces a whole component to rerender - so you should avoid it). So you can safely pass it into effect or reactive component like Show
or Switch
without worries about tracking.
const Comp = (props: ReactiveProps<{ a: number }>) => (
<Show when={() => props.a > 10}>{(v) => v + 10}</Show>
);
const B = reactifyLite(Comp);
<B a={$(() => a.value + b.value)} />;
Troubleshooting
Third party libraries is not working with HOCs while using @preact-signals/safe-react
or @preact/signals-react
with babel
If you are using reactifyLite
or withSignalProps
and you component has no reaction on signal changes, probably parent the component has no signals tracking.
@preact-signals/safe-react
solution is to wrap component with HOC withTrackSignals
to ensure that all signals will be tracked.
import { withTrackSignals } from "@preact-signals/safe-react/manual";
import { withSignalProps } from "@preact-signals/utils/hocs";
const View$ = withSignalProps(withTrackSignals(View));
@preact/signals-react
has no withTrackSignals
HOC, but we can use this workaround:
import { useSignals } from "@preact/signals-react/runtime";
const _View = (...args) => View(...args);
const View$ = withSignalProps(_View);
If you wrapped some component with
Heavily inspired by:
License
@preact-signals/utils
is licensed under the MIT License, and you're free to use, modify, and distribute it under the terms outlined in the LICENSE file.