Skip to main content
Solar hooks let you add state and side effects to any component. Instead of a single general-purpose useEffect that runs after every render, Solar provides three purpose-built effect primitives: one for async data fetching, one for event listener management, and one for memoized computation. This narrower API means each hook can state exactly what it does, what it depends on, and how it cleans up — which makes component behavior predictable for both you and any AI agent reading the code.

useState

useState manages a single piece of reactive state. Call it at the top of your render function with an initial value. It returns a tuple: the current value and a setter function.
const [count, setCount] = useState(0)
The setter accepts either a direct value or a function of the previous value — the functional updater form is useful when the new value depends on the old one:
// direct value
setCount(5)

// functional updater — avoids stale closure issues
setCount(prev => prev + 1)
When you call the setter, Solar schedules a re-render of the component. If the new value is strictly equal to the previous value, no re-render is scheduled. Here is a complete Counter component that uses useState with the functional updater form:
import { createElement, useState, defineComponent, registry } from './solar/index.js'

const Counter = defineComponent({
  name: 'Counter',
  props: {
    initialCount: { type: 'number', default: 0 },
  },
  render({ initialCount }) {
    const [count, setCount] = useState(initialCount)

    return createElement('div', { class: 'counter' },
      createElement('p',   {}, `Count: ${count}`),
      createElement('button', { onclick: () => setCount(c => c - 1) }, '−'),
      createElement('button', { onclick: () => setCount(c => c + 1) }, '+'),
    )
  },
})

registry.register(Counter)
export default Counter

useResource

useResource handles async data fetching. Pass it a key and a fetch function. Solar calls the fetch function with an AbortSignal and returns { data, loading, error }.
const { data, loading, error } = useResource({
  key: userId,           // re-fetches when this changes
  fetch: async (signal) => {
    const res = await fetch(`/api/users/${userId}`, { signal })
    return res.json()
  },
})
When key changes between renders, Solar calls controller.abort() on the previous in-flight request automatically before starting the new one. You do not need to manage the AbortController yourself — just pass signal to your fetch call. The returned state evolves as the request progresses:
Phaseloadingdataerror
Request in flighttrueprevious value or nullnull
Request succeededfalseresolved valuenull
Request failedfalseprevious value or nullError object
Use loading and error to render the appropriate UI state:
if (loading) return createElement('p', {}, 'Loading...')
if (error)   return createElement('p', {}, `Error: ${error.message}`)
return createElement('p', {}, data.name)

useSubscription

useSubscription attaches an event listener and manages its full lifecycle. Pass a source, an event name, and a handler function.
useSubscription({
  source:  window,
  event:   'resize',
  handler: () => setWidth(window.innerWidth),
})
Solar attaches the listener on the first render. On subsequent renders, it only re-attaches if source, event, or handler has changed. When the component unmounts, Solar detaches the listener automatically — you never need to call removeEventListener yourself. useSubscription works with both DOM EventTarget objects (via addEventListener/removeEventListener) and Node-style emitters (via on/off), so you can use it with WebSocket wrappers, event buses, or any object that follows either convention.

useMemo

useMemo caches the result of an expensive computation and only recomputes it when its dependencies change.
const formatted = useMemo(
  () => data.map(item => item.name.toUpperCase()),
  [data]
)
The first argument is a zero-argument function that returns the computed value. The second argument is a dependency array — Solar compares each element with strict equality (===) against the previous render’s deps. If all elements are identical, Solar returns the cached value without calling the function again. Use useMemo for array transformations, string formatting, or any computation that would be slow to repeat on every render. Avoid putting side effects inside the compute function — use the dedicated effect hooks instead.

Hooks rules

Solar hooks follow the same calling rules as React hooks, for the same reasons: Solar tracks hooks by their call order within a render cycle.
You must follow these rules or Solar will mix up hook state between renders:
  1. Call hooks inside render only. Never call useState, useResource, useSubscription, or useMemo outside of a component’s render function.
  2. Never call hooks conditionally. Do not put a hook call inside an if block, a loop, or a nested function. Every render must call the same hooks in the same order.
  3. Hooks must always run in the same order. The first hook call in render is always slot 0, the second is slot 1, and so on. Conditional execution breaks this indexing.
onMount and onUnmount are lifecycle hooks, not render-time state or effect hooks. They follow the same calling rules but serve a different purpose — running code once after the first render (onMount) or registering a cleanup that runs when the component is removed from the DOM (onUnmount). See the Lifecycle page in the API Reference for full details.