@preact-signals/safe-react
v0.8.1
Published
Manage state with style in React
Downloads
8,409
Maintainers
Readme
@preact-signals/safe-react
This is community driven preact/signals integration for React, based on official @preact/signals-react
integration, since it's patching react - there are a lot of problems in different environments and bundlers. This package tries to solve this problem by this steps:
- no runtime react internals patching
- uses babel/swc plugin to subscribe your components to signals (based on official
@preact/signals-react-transform
). - if environment doesn't support babel/swc plugin - exports HOC to subscribe your components to signals
- achieves the same features by bundler aliasing for react
Signals
Signals is a performant state management library with two primary goals:
- Make it as easy as possible to write business logic for small up to complex apps. No matter how complex your logic is, your app updates should stay fast without you needing to think about it. Signals automatically optimize state updates behind the scenes to trigger the fewest updates necessary. They are lazy by default and automatically skip signals that no one listens to.
- Integrate into frameworks as if they were native built-in primitives. You don't need any selectors, wrapper functions, or anything else. Signals can be accessed directly and your component will automatically re-render when the signal's value changes.
Read the announcement post to learn more about which problems signals solves and how it came to be.
There are two ways of tracking signals:
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
Table of Contents
SWC plugin compatibility table
| next
| @preact-signals/safe-react
| @swc/core
|
|--------|------------------------------|--------------|
| ^14.0.0
| 0.7.0
| - |
| 15.0.3
| ~0.8.0
| 1.8.0-1.9.2
|
React Integration features
Note: please open an issue here if in some scenario you have problems with this integration.
Tracking
The React adapter allows you to access signals directly inside your components and will automatically subscribe to them.
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 <p>Value: {count.value}</p>;
}
Hooks
If you need to instantiate new signals inside your components, you can use the useSignal
or useComputed
hook.
import { useSignal, useComputed } from "@preact-signals/safe-react";
function Counter() {
const count = useSignal(0);
const double = useComputed(() => count.value * 2);
return (
<button onClick={() => count.value++}>
Value: {count.value}, value x 2 = {double.value}
</button>
);
}
Optimization: Put signal into JSX
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.
import { signal } from "@preact-signals/safe-react";
const count = signal(0);
// Unoptimized: Will trigger the surrounding
// component to re-render
function Counter() {
return <p>Value: {count.value}</p>;
}
// Optimized: Will diff only value of signal
function Counter() {
return (
<p>
<>Value: {count}</>
</p>
);
}
Prop signal unwrapping
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.
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 = () => <div data-count={count}>Value: {count.value}</div>;
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.
const A = () => {
const count = signal(0);
count.value++;
return <div>{count.value}</div>;
};
Installation:
npm install @preact-signals/safe-react
Integrations:
- Automatic
- Manual (next.js, webpack, etc)
Next.js integration
/** @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;
Vite integration (swc)
// 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", {}]],
}),
],
});
Vite integration (babel)
// 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"],
},
}),
],
});
Vite props unwrapping (deprecated)
// 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"],
},
}),
],
});
Vite integration trackings signals in node_modules
Allows to transpile components that uses @useSignals
in node_modules (For example: @preact-signals/utils
)
// 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", {}]],
}),
],
});
React Native integration
yarn add -D babel-plugin-module-resolver
// 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",
],
};
Manual integration
import { withTrackSignals } from "@preact-signals/safe-react/manual";
const A = withTrackSignals(() => {
const count = signal(0);
count.value++;
return <div>{count.value}</div>;
});
How it works
Automatic integration
Magic contains 2 parts:
- parser plugin. Which transforms your components to subscribe to signals
It will be transformed to:
const sig = signal(0);
const A = () => <div>{sig.value}</div>;
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 <div>{sig.value}</div>;
} finally {
effectStore[EffectStoreFields.finishTracking]();
}
};
- (Deprecated) jsx runtime. Which unwraps signals while it passed as props to elements
const sig = signal(0);
// data-a={sig} will be unwrapped and equal to data-a={sig.value}
const A = () => <div data-a={sig}>{sig.value}</div>;
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
- Components: components which contains
manual
- none of hooks or components are tracked by default. You can use@useSignals
comment to track signals
// @useSignals
const Component = () => <div />
How to options mode
- babel
{
"plugins": [
[
"module:@preact-signals/safe-react/babel",
{
"mode": "manual"
}
]
]
}
- swc
[ "@preact-signals/safe-react/swc", { "mode": "manual" } ]
SWC specific options
transformHooks
- default: true
true
- transform hooks which uses.value
accessfalse
- don't transform hooks
[
"@preact-signals/safe-react/swc",
{
"transformHooks": false
}
]
How parser plugin detects components?
- function starting with capital letter
- function uses jsx syntax
// will be transformed
const A = () => <div>{sig.value}</div>;
// will not be transformed
const a = () => <div>{sig.value}</div>;
// will be transformed
/**
* @useSignals
*/
const b = () => <div>{sig.value}</div>;
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:
import { withTrackSignals } from "@preact-signals/safe-react/manual";
const A = withTrackSignals(() => {
const count = useSignal(0);
count.value++;
return <div>{count.value}</div>;
});
// 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 <div>{count.value}</div>;
} finally {
effectStore[EffectStoreFields.finishTracking]();
}
};
Troubleshooting
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
"use client";
const A = () => <div>{sig.value}</div>;
- opt out from tracking with
@noUseSignals
directive`
/**
* @noUseSignals
*/
const Page = () => (
<head>
<title>Page title</title>
</head>
);
- use
auto
mode of plugin, to transform only components which uses.value
access. How parser plugin detects components?
/** @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)
const Page = async () => (
<head>
<title>Page title</title>
</head>
);
Next.js double rendering
/**
* @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
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.
const sig = signal(0);
const A = ({ renderButton }: { renderButton: () => JSX.Element }) =>
sig.value % 2 ? renderButton() : <div>{sig.value}</div>;
const B = () => <button>Some content</button>;
<A renderButton={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
torenderB
and use it asrenderButton={renderB}
. Since transform transforms only function starting with capital letter. - use
React.createElement(B)
instead ofB()
- Add
@noUseSignals
directive toB
function
/**
* @noUseSignals
*/
const B = () => <button>Some content</button>;
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.
License
MIT
, see the LICENSE file.