Renderer hooks
Two hooks cover most needs: useInvoke for request/response, useEvent for pushed events. Both return signals rather than framework-specific state.
useInvoke
import { useInvoke } from '@cc-heart/electron-use-ipc/renderer'
import { api } from '../shared/api'
const { data, error, loading, call } = useInvoke(api.invoke.getUser)
await call('1') // (id: string) => Promise<User>
data.get() // User | undefined
| Field | Type | Notes |
|---|---|---|
data | Signal<R | undefined> | Last resolved value |
error | Signal<unknown | undefined> | Last thrown value |
loading | Signal<boolean> | True while the call is in flight |
call | (...args: A) => Promise<R> | Runs the method; args inferred from the token |
call rethrows on failure. That means you can await it for control flow and observe the same failure through the error signal.
Immediate calls
Pass immediate to run the method as soon as the hook is created. The arguments are still type-checked against the token:
const { data } = useInvoke(api.invoke.listUsers, { immediate: [] })
// data: Signal<User[] | undefined>
useEvent
import { useEvent } from '@cc-heart/electron-use-ipc/renderer'
const updated = useEvent(api.events['user:updated'])
// Signal<User | undefined>
updated.get() // latest payload, or undefined before the first
updated.subscribe((u) => { ... }) // fires on every push
invoke
When you do not need reactive state, call a method directly:
import { invoke } from '@cc-heart/electron-use-ipc/renderer'
const user = await invoke(api.invoke.getUser, '1')
Signals
Signal<T> is the primitive behind both hooks — a value, a setter, and a subscriber set. It is around thirty lines, with no dependency and no framework coupling:
/** A tiny framework-agnostic reactive primitive (signal). */
export type Listener<T> = (value: T) => void
export interface Signal<T> {
/** Read the current value. */
get(): T
/** Set a new value (or update via function). Notifies subscribers. */
set(value: T | ((prev: T) => T)): void
/** Subscribe to changes. Returns an unsubscribe function. */
subscribe(listener: Listener<T>): () => void
}
export function signal<T>(initial: T): Signal<T> {
let value = initial
const listeners = new Set<Listener<T>>()
return {
get: () => value,
set(next) {
const resolved =
typeof next === 'function' ? (next as (p: T) => T)(value) : next
if (Object.is(resolved, value)) return
value = resolved
for (const l of listeners) l(value)
},
subscribe(listener) {
listeners.add(listener)
return () => {
listeners.delete(listener)
}
},
}
}
get() reads the current value, set() writes it and notifies subscribers (skipping the notification when the value is Object.is-equal), and subscribe() returns an unsubscribe function.
Framework adapters
Because the hooks return signals, binding them to a component tree is a few lines. The two adapters below are complete — neither the core nor the renderer entry imports a UI framework.
/**
* Adapter for React — the library returns framework-agnostic `Signal`s, so
* binding them to a component tree is a handful of lines.
*
* Illustrative content for the landing page; not compiled.
*/
import { useSyncExternalStore } from 'react'
import type { Signal } from '@cc-heart/electron-use-ipc/core'
import { useInvoke, useEvent } from '@cc-heart/electron-use-ipc/renderer'
import { api } from './shared/api'
/** Subscribe a component to a signal. `subscribe` returns an unsubscribe fn. */
export function useSignalValue<T>(signal: Signal<T>): T {
return useSyncExternalStore(
(cb) => signal.subscribe(cb),
() => signal.get(),
)
}
export function UserCard({ id }: { id: string }) {
const { data, loading, call } = useInvoke(api.invoke.getUser)
const user = useSignalValue(data)
// `call` is (id: string) => Promise<User> — inferred from the token.
React.useEffect(() => {
void call(id)
}, [id])
if (loading.get()) return <p>Loading…</p>
return <p>{user?.name}</p>
}
/**
* Adapter for Vue — same signals, bound with `shallowRef`.
*
* Illustrative content for the landing page; not compiled.
*/
import { onScopeDispose, shallowRef, watchEffect } from 'vue'
import type { Signal } from '@cc-heart/electron-use-ipc/core'
import { useEvent, useInvoke } from '@cc-heart/electron-use-ipc/renderer'
import { api } from './shared/api'
/** Turn a signal into a Vue ref that stays in sync. */
export function useSignalRef<T>(signal: Signal<T>) {
const value = shallowRef(signal.get())
const unsubscribe = signal.subscribe((next) => {
value.value = next
})
onScopeDispose(unsubscribe)
return value
}
export function useUser(id: string) {
const { data, loading, call } = useInvoke(api.invoke.getUser)
// `call` is (id: string) => Promise<User> — inferred from the token.
watchEffect(() => void call(id))
const user = useSignalRef(data)
const updated = useEvent(api.events['user:updated'])
return { user, updated, loading: useSignalRef(loading) }
}
The important detail in both is calling the unsubscribe function on teardown. subscribe returns one; useSyncExternalStore and Vue’s onScopeDispose are the hooks that wire it up.
Next
API reference — every export, plus the channel and error semantics.