Manage state with style in React
npm install @preact-signals/safe-react@preact-signals/safe-react@preact/signals-react integration.
@vitejs/plugin-react-swc with a Rust-based plugin
@preact/signals-react-transform).
automatic - using swc/babel plugin to subscribe your components to signals (based on official @preact/signals-react-transform).
manual - manual adding tracking to your components with HOC
signal(initialValue)
signal.peek()
computed(fn)
effect(fn)
batch(fn)
untracked(fn)
next | @preact-signals/safe-react | @swc/core |
^14.0.0 | 0.7.0 | - |
15.0.3..15.1.7 | ~0.8.0 | 1.8.0-1.9.2|
15.2.0..16.0.0 | ~0.9.0 | 1.11.1 |
>=16.0.0* | ~0.10.0 | 1.13.21 |
js
import { signal } from "@preact-signals/safe-react";
const count = signal(0);
function CounterValue() {
// Whenever the count signal is updated, we'll
// re-render this component automatically for you
return Value: {count.value}
;
}
`
$3
If you need to instantiate new signals inside your components, you can use the useSignal or useComputed hook.
`js
import { useSignal, useComputed } from "@preact-signals/safe-react";
function Counter() {
const count = useSignal(0);
const double = useComputed(() => count.value * 2);
return (
);
}
`
$3
The React adapter ships with several optimizations it can apply out of the box to minimize virtual-dom diffing. If you pass a signal directly into JSX, it will behave as component which renders value of signal.
`js
import { signal } from "@preact-signals/safe-react";
const count = signal(0);
// Unoptimized: Will trigger the surrounding
// component to re-render
function Counter() {
return Value: {count.value}
;
}
// Optimized: Will diff only value of signal
function Counter() {
return (
<>Value: {count}>
);
}
`
$3
If you pass a signal as a prop to a component, it will automatically unwrap it for you. This means you can pass signals directly to DOM elements and they will be bound to the DOM node.
`js
import { signal } from "@preact-signals/safe-react";
const count = signal(0);
// data-count={count} will be unwrapped and equal to data-count={count.value}
const Counter = () => Value: {count.value};
`
Comparison table:
| Feature | @preact/signals-react | @preact-signals/safe-react (automatic) | @preact-signals/safe-react (manual) |
| ------------------- | ---------------------------------- | ---------------------------------------- | ------------------------------------- |
| Monkey patch free | ✅ (after 2.0.0 with babel plugin) | ✅ | ✅ |
| Tracking type | automatic | automatic | manual with HOC |
| Hooks | ✅ | ✅ | ✅ |
| Prop unwrapping | ❌ (removed in 2.0.0) | ✅(deprecated) | ❌ |
| Put signal into JSX | ✅ | ✅ | ✅ |
Alterations from
@preact/signals-react
Ignoring updates while rendering same component in render. Since this behavior causes double or infinite rerendering in some cases.
`tsx
const A = () => {
const count = signal(0);
count.value++;
return {count.value};
};
`
Installation:
`sh
npm install @preact-signals/safe-react
`
Integrations:
- Automatic
- Next.js
- Vite swc
- Vite babel
- Vite with @preact-signals/utils
- react-native
- Manual (next.js, webpack, etc)
$3
Integration playground
`js
/* @type {import('next').NextConfig} /
const nextConfig = {
experimental: {
swcPlugins: [
[
"@preact-signals/safe-react/swc",
{
// you should use auto mode to track only components which uses .value access.
// Can be useful to avoid tracking of server side components
mode: "auto",
} / plugin options here /,
],
],
},
};
module.exports = nextConfig;
`
$3
Integration playground
`ts
// vite.config.ts
import { defineConfig } from "vite";
import reactSwc from "@vitejs/plugin-react-swc";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
reactSwc({
plugins: [["@preact-signals/safe-react/swc", {}]],
}),
],
});
`
$3
`ts
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react({
babel: {
plugins: ["module:@preact-signals/safe-react/babel"],
},
}),
],
});
`
$3
`ts
// vite.config.ts
import { defineConfig } from "vite";
// can be used with swc plugin, too
import react from "@vitejs/plugin-react";
import { createReactAlias } from "@preact-signals/safe-react/integrations/vite";
// https://vitejs.dev/config/
export default defineConfig({
resolve: {
// add this
alias: [createReactAlias()],
},
plugins: [
react({
// add this
jsxImportSource: "@preact-signals/safe-react/jsx",
babel: {
plugins: ["module:@preact-signals/safe-react/babel"],
},
}),
],
});
`
$3
Allows to transpile components that uses @useSignals in node_modules (For example: @preact-signals/utils)
Integration playground
`ts
// vite.config.ts
import { defineConfig } from "vite";
import reactSwc from "@vitejs/plugin-react-swc";
import { createSWCTransformDepsPlugin } from "@preact-signals/safe-react/integrations/vite";
// https://vitejs.dev/config/
export default defineConfig({
resolve: {
alias: [
// if some lib uses signals it's probably using @preact/signals-react
{
find: "@preact/signals-react",
replacement: "@preact-signals/safe-react",
},
],
},
plugins: [
createSWCTransformDepsPlugin({
filter: (id) => id.includes("node_modules"),
}),
reactSwc({
plugins: [["@preact-signals/safe-react/swc", {}]],
}),
],
});
`
$3
`sh
yarn add -D babel-plugin-module-resolver
`
`js
// babel.config.js
module.exports = {
// or expo-preset or metro-react-native-babel-preset
presets: ["@rnx-kit/babel-preset-metro-react-native"],
plugins: [
[
"module-resolver",
{
alias: [
{
"@preact/signals-react": "@preact-signals/safe-react",
},
],
},
],
"module:@preact-signals/safe-react/babel",
],
};
`
$3
`tsx
import { withTrackSignals } from "@preact-signals/safe-react/manual";
const A = withTrackSignals(() => {
const count = signal(0);
count.value++;
return {count.value};
});
`
$3
#### Automatic integration
Magic contains 2 parts:
- parser plugin. Which transforms your components to subscribe to signals
It will be transformed to:
`tsx
const sig = signal(0);
const A = () => {sig.value};
`
`tsx
import { useSignals } from "@preact-signals/safe-react/tracking";
const sig = signal(0);
const A = () => {
const store = useSignals();
try {
// all signals used in this function will be tracked
return {sig.value};
} finally {
effectStore[EffectStoreFields.finishTracking]();
}
};
`
- (Deprecated) jsx runtime. Which unwraps signals while it passed as props to elements
`tsx
const sig = signal(0);
// data-a={sig} will be unwrapped and equal to data-a={sig.value}
const A = () => {sig.value};
`
#### How parser plugins works
Supported parsers:
- swc
- babel
Parser plugin transforms your components to subscribe to signals. It works in 3 modes:
- all (default)
- Components: will be wrapped with try/finally block to track signals
- Hooks (if transformHooks: true): all hooks that accesses .value will be wrapped with try/finally block to track signals
- auto
- Components: components which contains .value access will be wrapped with try/finally block to track signals
- Hooks (if transformHooks true) that which contains .value access will be wrapped with try/finally block to track signals
- manual - none of hooks or components are tracked by default. You can use @useSignals comment to track signals
`ts
// @useSignals
const Component = () =>
`
##### How to options mode
- babel
`json
{
"plugins": [
[
"module:@preact-signals/safe-react/babel",
{
"mode": "manual"
}
]
]
}
`
- swc
`json
[
"@preact-signals/safe-react/swc",
{
"mode": "manual"
}
]
`
#### SWC specific options
transformHooks - default: true
- true - transform hooks which uses .value access
- false - don't transform hooks
`json
[
"@preact-signals/safe-react/swc",
{
"transformHooks": false
}
]
`
##### How parser plugin detects components?
- function starting with capital letter
- function uses jsx syntax
`tsx
// will be transformed
const A = () => {sig.value};
// will not be transformed
const a = () => {sig.value};
// will be transformed
/**
* @useSignals
*/
const b = () => {sig.value};
`
You can use @useSignals to opt-in to tracking for a component that doesn't meet the criteria above.
Or you can use @noUseSignals to opt-out of tracking for a component that does meet the criteria above.
#### Manual integration
Manual integration wraps your component in try/finally block via HOC. It's equal to:
`tsx
import { withTrackSignals } from "@preact-signals/safe-react/manual";
const A = withTrackSignals(() => {
const count = useSignal(0);
count.value++;
return {count.value};
});
// equal to
import { useSignals } from "@preact-signals/safe-react/tracking";
const A = () => {
const store = useSignals();
try {
// all signals used in this function will be tracked
const count = signal(0);
count.value++;
return {count.value};
} finally {
effectStore[EffectStoreFields.finishTracking]();
}
};
`
$3
#### Some of my components are not updating
- Manual integration: you need to wrap your component with withTrackSignals HOC
- Automatic integration:
Probably your component doesn't meet the criteria from How parser plugin detects components? section. You can use @useSignals to opt-in to tracking for a component that doesn't meet the criteria above.
#### Automatic integration with Server Components: Maybe one of these should be marked as a client entry with "use client":
Some of server side component is transformed to track signals.
Solutions:
- mark it as client side component with use client directive
`tsx
"use client";
const A = () => {sig.value};
`
- opt out from tracking with @noUseSignals directive
tsx
/**
* @noUseSignals
*/
const Page = () => (
Page title
);
`
- use auto mode of plugin, to transform only components which uses .value access. How parser plugin detects components?
`js
/* @type {import('next').NextConfig} /
const nextConfig = {
experimental: {
swcPlugins: [
[
"@preact-signals/safe-react/swc",
{
mode: "auto",
},
],
],
},
};
module.exports = nextConfig;
`
- not recommended because of performance overhead make component async (since component will be transformed only if it's sync)
`tsx
const Page = async () => (
Page title
);
`
#### Next.js double rendering
`tsx
/**
* @useSignals
*/
const PureComponent = () => {
// prints "render" twice on client side and once on server side
console.log("render");
return null;
};
`
It's happens because signals tracking uses useSyncExternalStore and for some reason it causes double rendering with Next.js strict mode. We can just to turn off strict mode in next.config.js
`js
module.exports = {
// other config
reactStrictMode: false,
};
`
#### Automatic integration: Rendered more hooks than during the previous render
This error occurs when you're using some component without hooks as render function conditionally.
`tsx
const sig = signal(0);
const A = ({ renderButton }: { renderButton: () => JSX.Element }) =>
sig.value % 2 ? renderButton() : {sig.value};
const B = () => ;
sig.value++; // this will cause error
`
It isn't working, because transform think that B is a component, but it's just a function. There're 3 ways to fix this:
- rename B to renderB and use it as renderButton={renderB}. Since transform transforms only function starting with capital letter.
- use React.createElement(B) instead of B()
- Add @noUseSignals directive to B function
`tsx
/**
* @noUseSignals
*/
const B = () => ;
`
#### Error: Cannot update a component (Component) while rendering a different component (Component2). To locate the bad setState() call inside Component2
This error occurs when you're updating another component in render time of another component. In most case you should ignore this message, since it's just warning
To opt into this optimization, simply pass the signal directly instead of accessing the .value property.
> Note
> The content is wrapped in a React Fragment due to React 18's newer, more strict children types.
#### Next.js comments
Opt-in and opt-out declarations are unsupported in server components due to the issue. Next.js strips comments for server component files
License
MIT`, see the LICENSE file.