React Bindings
We provide officially-supported React bindings for signia
in two packages:
signia-react
provides hooks for creating and consuming signals in functional components.useAtom
- A hook for creating atomic signals.useComputed
- A hook for creating computed signals.track
- component wrapper for automatically tracking signal value access and re-rendering the wrapped component if the signals' values change.useValue
- A hook for manually tracking signal value access (not required if you usetrack
)
signia-react-jsx
provides a minimal global jsx integration for use with TypeScript'sjsxImportSource
option. This causes all functional components to be automatically tracked. It does not provide any automatic unpacking (i.e. dereferencing) of signal values.- ✅
<div>Your name is {name.value}</div>
— Correct - ❌
<div>Your name is {name}</div>
— We do not call.value
for you
- ✅
If you haven't already, see the installation guide for instructions on how to install and set up the packages.
Usage
Before reading this section, make sure you have read the Using Signals tutorial.
Writing reactive components
If you are using signia-react-jsx
, feel free to skip this section. Your components are already reactive! ✨
(If you're using Remix, please see the below tips.)
We recommend using track
to wrap all components that use signals following this pattern:
type MyComponentProps = { foo: Bar }
const MyComponent = track(function MyComponent(props: MyComponentProps) {
// ...
})
If you are unable to use track
, you should make sure that any usages of signals are wrapped by a useValue
type MyComponentProps = { foo: Signal<Bar> }
const MyComponent: React.FC<MyComponentProps> = (props: MyComponentProps) => {
const foo: Bar = useValue(props.foo)
// ...
}
If a signal is being used indirectly you can pass a 'compute' function to useValue
.
type MyComponentProps = { getFoo: (n: number) => Bar }
const MyComponent: React.FC<MyComponentProps> = (props: MyComponentProps) => {
// getFoo uses a signal under the hood, but we don't have direct access to that signal.
const n = 42
const foo: Bar = useValue('foo', () => props.getFoo(42), [props.getFoo, n])
// ...
}
Setting up reactivity with jsxImportSource
may take some extra care with Remix.
Add the following settings to your
tsconfig.json
file:"jsxImportSource": "signia-react-jsx",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,The first modifies the JSX pragma to add Signia's
track
function to all your components. The second two allow you to use the@computed
decorator when building classes around Signia'satoms
.Add the following setting to your
remix.config.js
file to prevent your build from failing:serverDependenciesToBundle: ["signia-react-jsx/jsx-dev-runtime"],
At present, the updated pragma won't affect a Remix module's default component, but it will affect its children. This gives you several options. You can:
a. Wrap each module's default component in
track
, then rely on the pragma in their children.b. Rely on the
useValue
hook in each module's default component and the pragma in their children.c. Always rely on the pragma by using your module's default component to wrap a child. For instance, this:
export default function MyRoute() {
return <AllMyRouteLogicAndComponents />;
}instead of this:
export default function MyRoute() {
const loaderData = useLoaderData<typeof loader>();
const signiaState = SigniaClass.useSigniaState(loaderData);
const stringArray = signiaState.myStringArrayData;
return (
<Layout>
{stringArray.map((str) => {
return <p key={str}>{str}</p>;
})}
</Layout>
);
}
Managing shared state
We recommend keeping high-level shared state and logic in a class, or a set of linked classes.
import { atom } from 'signia'
class Document {
private readonly state = atom('Document.state', {
title: 'Page 1',
body: 'words etc',
})
readonly stylePanel = new StylePanel(this)
setTitle(title: string) {
this.state.update((state) => ({ ...state, title }))
}
setBody(body: string) {
this.state.update((state) => ({ ...state, body }))
}
}
class StylePanel {
constructor(private document: Document) {}
private readonly state = atom('StylePanel.state', {
fontSize: 12,
color: 'black',
})
increaseFontSize() {
this.state.update((state) => ({ ...state, fontSize: state.fontSize + 1 }))
}
}
Then creating a hook to instantiate it when your app initializes:
const useNewDocument = () => useMemo(() => new Document(), [])
const App = () => {
const doc = useNewDocument()
// ...
}
You can either pass the doc around in props or use context to make it more easily accessible without prop drilling.
We prefer to use context:
const DocumentContext = React.createContext<Document | null>(null)
class Document {
// ...
static useNewDocument = () => {
const doc = useMemo(() => new Document(), [])
// You can add any effects and other lifecycle logic in here
return doc
}
}
const useDocument = () => {
const doc = useContext(DocumentContext)
if (!doc) throw new Error('No document found in context')
return doc
}
const App = () => {
const doc = Document.useNewDocument()
return (
<DocumentContext.Provider value={doc}>
{/* ... the rest of the app ... */}
</DocumentContext.Provider>
)
}
Managing local state
useAtom
can help you manage component-local state in a similar way to useState
.
const Counter = track(function Counter () {
- const [count, setCount] = useState(0)
+ const count = useAtom('count', 0)
- const increment = useCallback(() => setCount(count + 1), [count])
+ const increment = useCallback(() => count.set(count.value + 1), [])
- return <button onClick={increment}>The count is {count}</button>
+ return <button onClick={increment}>The count is {count.value}</button>
})
In this example, count
will never change and count.value
will always be up to date, so you never need to worry about stale values in closures.
You can think of useAtom
as a 'reactive' version of React.useRef
.
Avoiding unwanted re-renders
Signals are as fine-grained as you make them. Very often you might have a signal that contains an object, but you only care about part of the object.
class Document {
state = atom('Document.state', { title: 'Page 1', body: 'words etc' })
setTitle(title: string) {
this.state.update((state) => ({ ...state, title }))
}
setBody(body: string) {
this.state.update((state) => ({ ...state, body }))
}
}
const DocumentTitle = track(function DocumentTitle({ doc }: { doc: Document }) {
return <h1>{doc.state.value.title}</h1>
})
In this example, every time setBody
is called it will cause the DocumentTitle
component to re-render, even though it does not use the body text. This is because there is only one signal at play here: the atom for the whole document state. DocumentTitle
accesses that signal directly so it rerenders any time the whole document state changes.
To get around this, you can create a computed signal to 'select' the part of the state you care about. There are a number of ways to do that:
[recommmended] Use the
@computed
annotation in theDocument
classclass Document {
private readonly state = atom('Document.state', {
title: 'Page 1',
body: 'words etc',
})
@computed get title() {
return this.state.value.title
}
}
const DocumentTitle = track(function DocumentTitle({ doc }: { doc: Document }) {
return <h1>{doc.title}</h1>
})Extract the title with
useValue
.const DocumentTitle: React.FC<{ doc: Document }> = ({ doc }) => {
const title = useValue('title', () => doc.state.value.title, [doc])
return <h1>{title}</h1>
}Extract the title with
useComputed
.const DocumentTitle = track(function DocumentTitle({ doc }: { doc: Document }) {
const title = useComputed('title', () => doc.state.value.title, [doc])
return <h1>{title.value}</h1>
})
Running effects without re-rendering the component
useEffect
is a good way to run effects after re-rendering the component. With Signia you can use signal values in useEffect dependency arrays, as you would any other values.
const DocumentTitle = track(function DocumentTitle({ doc }: { doc: Document }) {
// set the browser tab's title
useEffect(() => {
window.document.title = doc.title
}, [doc.title]])
return <h1>{doc.title}</h1>
})
However, sometimes you might wish to avoid triggering a re-render just to execute some effect. In that case you can combine useEffect
with react
.
import { react } from 'signia'
const SetDocumentTitle: React.FC<{ doc: Document }> = () => {
useEffect(
() =>
react(() => {
window.document.title = doc.title
}),
[doc]
)
return null
}