Vue's mutable reactivity model for React on the web and React Native.
Rue re-exports @vue/reactivity and adds React hooks that connect refs,
reactive objects, computed values, watchers, and effect scopes to the React
lifecycle. Rendering stays ordinary React and JSX.
import{useComputed,useReactive}from'@themakers/rue'exportfunctionCounter(){conststate=useReactive({count: 0})constdoubled=useComputed(()=>state.count*2)return(<buttononClick={()=>state.count++}>{state.count} / {doubled.value}</button>)}| Platform | Status | Validation |
|---|---|---|
| React DOM | Supported | Vitest, jsdom, Vite example |
| React Native | Supported | jest-expo, React Native Testing Library, Expo example |
| Hermes | Supported | Hermes bytecode compile and Expo Android Hermes bundle |
| React Strict DOM | Supported | Expo + RSD example and Android Hermes bundle |
| SSR | Architecture-ready | getServerSnapshot and renderToString smoke test |
| React Server Components | Not supported | Outside the current scope |
The runtime imports only react and @vue/reactivity. It has no dependency on
react-dom, react-native, browser globals, or renderer-specific batching.
pnpm add @themakers/rue react @vue/reactivityRue can also be installed directly from a pinned git commit before an npm release exists:
{
"dependencies": {
"@themakers/rue": "github:themakers/rue#<commit-sha>"
}
}The repository does not commit dist. During an npm or pnpm git installation,
prepare uses the installed development toolchain to generate the package.
Bun does not install a git dependency's development dependencies, so Rue's
trusted prepare automatically falls back to Bun's built-in bundler and its
source declarations:
bun add --trust github:themakers/rue#<commit-sha>If the application declares trustedDependencies manually, include
@themakers/rue while retaining any other packages whose install scripts the
application needs. prepack always performs the full build and validates the
output before creating a package tarball. Pin a commit SHA so installs remain
reproducible.
Requirements:
- React 18 or 19;
@vue/reactivity3.5.x;- Proxy support, available in current browsers and Hermes.
CI tests the minimum React 18.3.1 peer, the latest React 19 web release, and the React/React Native versions pinned by the current Expo SDK. Expo applications should follow Expo's renderer version matrix rather than upgrading React Native independently.
react and @vue/reactivity are peer dependencies. Ensure the application has
only one resolved copy of @vue/reactivity, especially when Vue and Rue are
installed together. Separate copies maintain separate dependency graphs, so a
value created by one copy cannot notify effects owned by another.
Rue exports the complete public API of @vue/reactivity, plus:
| API | Purpose |
|---|---|
onMounted | Passive effect setup after mount |
onUpdated | Passive effect after every commit except the initial mount |
onBeforeUnmount | Passive effect cleanup |
useRef | Component-owned deep Vue ref |
useShallowRef | Component-owned shallow Vue ref |
useTemplateRef | Vue-style ref that binds directly to a JSX ref |
useCustomRef | Component-owned custom Vue ref |
useReactive | Component-owned deep reactive object |
useShallowReactive | Component-owned shallow reactive object |
useReadonly | Readonly view subscribed to its reactive source |
useShallowReadonly | Shallow readonly view |
useComputed | Component-owned computed ref |
watch | Synchronous Vue watch wrapper |
baseWatch | Direct @vue/reactivity watch export |
useWatch | Component-owned watch with automatic cleanup |
watchEffect | Synchronous Vue watch effect wrapper |
useWatchEffect | Component-owned watch effect with automatic cleanup |
useEffectScope | Component-owned effect scope facade |
useReactivity | Subscribe a component to module-level reactive data |
import{useComputed,useReactive,useRef,useWatch}from'@themakers/rue'exportfunctionProfile(){constname=useRef('Ada')conststate=useReactive({visits: 0})constlabel=useComputed(()=>`${name.value}: ${state.visits}`)useWatch(()=>state.visits,(visits)=>console.log('visits',visits),)return<buttononClick={()=>state.visits++}>{label.value}</button>}Initial arguments follow React state initializer semantics: changing an initializer on a later render does not replace the existing ref, proxy, or computed object.
useTemplateRef provides one ref for both JSX binding and Vue-style access.
React's renderer writes through the internal .current bridge while application
code reads the same element through .value:
import{onMounted,useTemplateRef}from'@themakers/rue'exportfunctionAutofocusInput(){constinput=useTemplateRef<HTMLInputElement>()onMounted(()=>input.value?.focus())return<inputref={input}/>}The ref starts at null, receives the host element after commit, and returns to
null when the element unmounts.
Create a normal Vue reactive value at module scope and subscribe each consumer
with useReactivity:
import{computed,reactive,useReactivity}from'@themakers/rue'constcounter=reactive({count: 0})constdoubled=computed(()=>counter.count*2)exportfunctionCounter(){conststate=useReactivity(()=>({ counter, doubled }))return(<buttononClick={()=>state.counter.count++}>{state.counter.count} / {state.doubled.value}</button>)}Rue uses useSyncExternalStore with a stable numeric version snapshot. A
reactive mutation invalidates subscribed components synchronously, allowing
React to detect an external-store change during a transition and restart the
render before commit.
Do not mutate external stores during render. For SSR, create request-local stores rather than sharing a mutable singleton between requests.
Raw watch and watchEffect run synchronously and return the Vue watch handle.
Their hook variants are installed after commit and stop automatically during
cleanup:
useWatchEffect((onCleanup)=>{constrequest=loadItem(itemId.value)onCleanup(()=>request.abort())})Vue's flush option is intentionally not supported. Rue has no Vue renderer
scheduler and does not emulate one with platform-specific APIs.
In development StrictMode, React may rehearse setup and cleanup. User effects
must follow the same idempotency rules as useEffect; Rue guarantees that each
backing watcher is stopped and that no discarded render leaves a watcher.
scope.run(setup) registers setup during render. Rue executes it after commit
inside a fresh Vue effect scope and stops that backing scope during cleanup:
import{onScopeDispose,useEffectScope,watch}from'@themakers/rue'constscope=useEffectScope()scope.run(()=>{consthandle=watch(source,consume)onScopeDispose(()=>console.log('scope disposed'))returnhandle})Because setup is deferred, scope.run() returns undefined. pause, resume,
and stop operate on the active backing scope. StrictMode can execute the setup
more than once with a complete disposal between runs.
Lifecycle helpers use passive React effects. Their development StrictMode semantics intentionally match React:
onMountedsetup may run twice;onBeforeUnmountmay observe the rehearsal cleanup and the real cleanup;onUpdatednever fires for either initial-mount setup, then runs after later commits.
The workspace contains three applications using the exact same
examples/shared-store package:
pnpm --filter @rue/example-web dev
pnpm --filter @rue/example-expo start
pnpm --filter @rue/example-expo-rsd startEach renderer also includes component-owned TODO state built with useReactive,
alongside the shared module store consumed through useReactivity.
Validate all examples:
pnpm examples:check
pnpm examples:bundleThe Expo Strict DOM example also includes an EAS Android + Maestro workflow that asserts the app is executing on Hermes and that a Rue mutation updates the UI.
pnpm install
pnpm run ciContributors need Node 22.13 or newer for the current Expo toolchain. The published ES2017 runtime itself supports Node 18+ and does not execute Node APIs inside applications.
Useful focused commands:
pnpm test:web
pnpm test:native
pnpm test:hermes
pnpm test:coverage
pnpm check:platform
pnpm check:sizeSee docs/audit.md,
docs/architecture.md, and
MIGRATION.md for design and compatibility details.
MIT. Rue is a fork of Veact and preserves the original copyright and attribution.