Skip to main content

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 use track)
  • signia-react-jsx provides a minimal global jsx integration for use with TypeScript's jsxImportSource 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

note

Before reading this section, make sure you have read the Using Signals tutorial.

Writing reactive components

tip

If you are using signia-react-jsx, feel free to skip this section. Your components are already reactive! ✨

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])
// ...
}

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:

  1. [recommmended] Use the @computed annotation in the Document class

    class 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>
    })
  2. 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>
    }
  3. 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
}