diff --git a/src/renderer/__tests__/hooks.node.test.tsx b/src/renderer/__tests__/hooks.node.test.tsx
index f0376c43..002393f9 100644
--- a/src/renderer/__tests__/hooks.node.test.tsx
+++ b/src/renderer/__tests__/hooks.node.test.tsx
@@ -1,4 +1,19 @@
-import React, { useCallback, useEffect, useId, useLayoutEffect, useMemo, useReducer, useRef, useState } from 'react'
+import React, {
+ createContext,
+ Usable,
+ use,
+ useActionState,
+ useCallback,
+ useEffect,
+ useEffectEvent,
+ useId,
+ useLayoutEffect,
+ useMemo,
+ useOptimistic,
+ useReducer,
+ useRef,
+ useState,
+} from 'react'
import { Text } from '../../jsx/Text'
import { renderVoltraVariantToJson } from '../renderer'
@@ -200,4 +215,85 @@ describe('Hooks', () => {
const output = renderVoltraVariantToJson()
expect(output.c).toBe('2')
})
+
+ test('16. use(context) reads context value', () => {
+ const ThemeContext = createContext('light')
+ const Consumer = () => {
+ const theme = use(ThemeContext)
+ return {theme}
+ }
+ const output = renderVoltraVariantToJson(
+
+
+
+ )
+ expect(output.c).toBe('dark')
+ })
+
+ test('17. use(context) without provider returns default', () => {
+ const ThemeContext = createContext('light')
+ const Consumer = () => {
+ const theme = use(ThemeContext)
+ return {theme}
+ }
+ const output = renderVoltraVariantToJson()
+ expect(output.c).toBe('light')
+ })
+
+ test('18. use(promise) throws', () => {
+ const Component = () => {
+ use(Promise.resolve('value'))
+ return test
+ }
+ expect(() => renderVoltraVariantToJson()).toThrow('use() with promises is not supported in Voltra')
+ })
+
+ test('19. use(unsupportedValue) throws', () => {
+ const Component = () => {
+ use('not a context or promise' as unknown as Usable)
+ return test
+ }
+ expect(() => renderVoltraVariantToJson()).toThrow('An unsupported type was passed to use()')
+ })
+
+ test('20. useActionState returns [initialState, dispatch, false]', () => {
+ const action = jest.fn()
+ let state, dispatch, isPending
+ const Component = () => {
+ ;[state, dispatch, isPending] = useActionState(action, { count: 0 })
+ return {state.count}
+ }
+ const output = renderVoltraVariantToJson()
+ expect(output.c).toBe('0')
+ expect(state).toEqual({ count: 0 })
+ expect(typeof dispatch).toBe('function')
+ expect(isPending).toBe(false)
+ dispatch()
+ expect(action).not.toHaveBeenCalled()
+ })
+
+ test('21. useOptimistic returns [passthrough, setter]', () => {
+ let value, setOptimistic
+ const Component = () => {
+ ;[value, setOptimistic] = useOptimistic('real')
+ return {value}
+ }
+ const output = renderVoltraVariantToJson()
+ expect(output.c).toBe('real')
+ expect(value).toBe('real')
+ expect(typeof setOptimistic).toBe('function')
+ setOptimistic('optimistic')
+ expect(value).toBe('real')
+ })
+
+ test('22. useEffectEvent returns callback identity', () => {
+ const fn = jest.fn()
+ let eventFn
+ const Component = () => {
+ eventFn = useEffectEvent(fn)
+ return test
+ }
+ renderVoltraVariantToJson()
+ expect(eventFn).toBe(fn)
+ })
})
diff --git a/src/renderer/dispatcher.ts b/src/renderer/dispatcher.ts
index 36beb270..f6d31d4c 100644
--- a/src/renderer/dispatcher.ts
+++ b/src/renderer/dispatcher.ts
@@ -2,23 +2,34 @@ import React, { Context, ReactDispatcher, ReactHooksDispatcher } from 'react'
import { ContextRegistry } from './context-registry.js'
+const REACT_CONTEXT_TYPE = Symbol.for('react.context')
+const REACT_MEMO_CACHE_SENTINEL = Symbol.for('react.memo_cache_sentinel')
+
declare module 'react' {
+ type HookFn = (...args: any[]) => any
+
export type ReactHooksDispatcher = {
- useState: typeof import('react').useState
- useReducer: typeof import('react').useReducer
- useEffect: typeof import('react').useEffect
- useLayoutEffect: typeof import('react').useLayoutEffect
- useInsertionEffect: typeof import('react').useInsertionEffect
- useCallback: typeof import('react').useCallback
- useMemo: typeof import('react').useMemo
- useRef: typeof import('react').useRef
- useContext: typeof import('react').useContext
- useId: typeof import('react').useId
- useImperativeHandle: typeof import('react').useImperativeHandle
- useDebugValue: typeof import('react').useDebugValue
- useDeferredValue: typeof import('react').useDeferredValue
- useTransition: typeof import('react').useTransition
- useSyncExternalStore: typeof import('react').useSyncExternalStore
+ useState: HookFn
+ useReducer: HookFn
+ useEffect: HookFn
+ useLayoutEffect: HookFn
+ useInsertionEffect: HookFn
+ useCallback: HookFn
+ useMemo: HookFn
+ useRef: HookFn
+ useContext: HookFn
+ useId: HookFn
+ useImperativeHandle: HookFn
+ useDebugValue: HookFn
+ useDeferredValue: HookFn
+ useTransition: HookFn
+ useSyncExternalStore: HookFn
+ use: HookFn
+ useActionState: HookFn
+ useOptimistic: HookFn
+ useEffectEvent: HookFn
+ useMemoCache: HookFn
+ useCacheRefresh: HookFn
}
export type ReactDispatcher = {
@@ -30,6 +41,27 @@ declare module 'react' {
export const getHooksDispatcher = (registry: ContextRegistry): ReactHooksDispatcher => ({
useContext: (context: Context) => registry.readContext(context),
+ use: (usable: React.Usable): T => {
+ if (
+ usable !== null &&
+ typeof usable === 'object' &&
+ (usable as { $$typeof?: symbol }).$$typeof === REACT_CONTEXT_TYPE
+ ) {
+ return registry.readContext(usable as unknown as Context)
+ }
+
+ if (
+ usable !== null &&
+ (typeof usable === 'object' || typeof usable === 'function') &&
+ typeof (usable as { then?: unknown }).then === 'function'
+ ) {
+ throw new Error(
+ 'use() with promises is not supported in Voltra. Async data fetching is not available in this synchronous renderer.'
+ )
+ }
+
+ throw new Error(`An unsupported type was passed to use(): ${String(usable)}`)
+ },
useState: (initial?: S | (() => S)) => [
typeof initial === 'function' ? (initial as () => S)() : initial,
() => {}, // No-op setter
@@ -58,6 +90,19 @@ export const getHooksDispatcher = (registry: ContextRegistry): ReactHooksDispatc
useSyncExternalStore: (_, getSnapshot) => {
return getSnapshot()
},
+ // No-op stubs for React 19 hooks
+ useActionState: (_: unknown, initialState: S, _permalink?: string) =>
+ [initialState, () => {}, false] as [S, () => void, boolean],
+ useOptimistic: (passthrough: T) => [passthrough, () => {}] as [T, (action: unknown) => void],
+ useEffectEvent: (callback: T): T => callback,
+ useMemoCache: (size: number) => {
+ const data = new Array(size)
+ for (let i = 0; i < size; i++) {
+ data[i] = REACT_MEMO_CACHE_SENTINEL
+ }
+ return data
+ },
+ useCacheRefresh: () => () => {},
})
export const getReactCurrentDispatcher = (): ReactDispatcher => {