Skip to content

feat(core,methods): add react-native framework adapter (#117)#137

Open
espetro wants to merge 3 commits into
open-circle:mainfrom
espetro:feature/117-react-native-framework-adapter
Open

feat(core,methods): add react-native framework adapter (#117)#137
espetro wants to merge 3 commits into
open-circle:mainfrom
espetro:feature/117-react-native-framework-adapter

Conversation

@espetro

@espetro espetro commented Jun 14, 2026

Copy link
Copy Markdown

Summary

Adds the React Native framework adapter to @formisch/core and wires up a React Native build target in both @formisch/core and @formisch/methods, per #117. This is the signal-layer foundation only — purely additive, no behaviour changes.

Changes

  • packages/core/src/framework/index.ts — add 'react-native' to the Framework union
  • packages/core/src/framework/index.react-native.ts (NEW) — adapter identical to index.react.ts with framework = 'react-native'
  • packages/core/package.json — expose ./react-native export
  • packages/core/tsdown.config.ts — add 'react-native' to the build matrix (added during impl; see note below)
  • packages/methods/tsdown.config.ts — add 'react-native' to the build matrix
  • packages/methods/package.json — expose ./react-native export

Note on the 6th file: the original plan only listed 5 files. The maintainer's #117 comment — "Both will then build and output a React Native specific version" — makes the core build-matrix update mandatory; without it pnpm -C packages/core build cannot produce dist/index.react-native.{js,d.ts} and the new @formisch/core/react-native export would 404. The edit is mechanical, identical in pattern to the methods one, and folded into commit 2.

Why

React Native shares React's rendering core, so the signal/batch/untrack primitives transfer verbatim. The existing rewriteFrameworkImports tsdown plugin (confirmed by @fabian-hiller in #117) auto-rewires @formisch/core@formisch/core/react-native for the RN build target, so no plugin work is needed.

Out of scope (follow-up)

Tracked for a separate frameworks/react-native PR:

  • FieldElement type and getElementInput RN-specific overrides in packages/core
  • useField event-handler adaptation (onChangeText / field.onChange per maintainer guidance)
  • Form component (View vs form element) and Field/TextInput/Switch/Picker adapters
  • New frameworks/react-native package, tests, and playground

Plan brief on the fork: espetro/formisch#1

Verification

  • pnpm -C packages/core build → produces dist/index.react-native.{js,d.ts}
  • pnpm -C packages/methods build → produces dist/index.react-native.{js,d.ts}
  • pnpm -C packages/core lint → clean ✅
  • pnpm -C packages/methods lint → clean ✅
  • pnpm -C packages/core test → 384 tests pass, 0 type errors ✅
  • pnpm -C packages/methods test → 157 tests pass, 0 type errors ✅
  • Runtime smoke check via direct dist import:
    initial: 0
    after batch: 2
    framework: react-native
    id sample: ifoha1knw9p
    

Commits

  1. feat(core): add react-native framework adapterpackages/core/src/framework/index.ts + new index.react-native.ts
  2. feat(core,methods): expose react-native build output — 4 package.json + tsdown.config.ts files

Closes #117


Summary by cubic

Adds a React Native framework adapter and React Native build targets for @formisch/core and @formisch/methods. This is the signal-layer foundation only with no behavior changes.

  • New Features
    • Added 'react-native' to the Framework union and a new packages/core/src/framework/index.react-native.ts adapter (same as React, with framework = 'react-native' and createSignal, batch, untrack, createId).
    • Exposed ./react-native exports and added React Native to the tsdown build matrix in both packages, producing dist/index.react-native.{js,d.ts} as required by Add React Native framework adapter #117.

Written for commit eea4c1c. Summary will update on new commits.

Review in cubic

espetro added 2 commits June 14, 2026 19:18
Extends the `Framework` union with 'react-native' and adds a
`index.react-native.ts` adapter identical to the React one
(`createSignal`, `batch`, `untrack`, `createId`) with only the
`framework` constant changed. This is the signal-layer foundation
for a future `@formisch/react-native` package; browser-specific
overrides (`FieldElement`, `getElementInput`) are intentionally
out of scope here.

Refs: open-circle#117
Adds 'react-native' to the per-framework build matrix in both
`packages/core/tsdown.config.ts` and `packages/methods/tsdown.config.ts`,
and exposes `./react-native` in both packages' export maps. The
`rewriteFrameworkImports` tsdown plugin rewires
`@formisch/core` -> `@formisch/core/react-native` in the methods
sources for the RN target, so no plugin work is required.

Produces `dist/index.react-native.{js,d.ts}` in both packages.

Refs: open-circle#117
@vercel

vercel Bot commented Jun 14, 2026

Copy link
Copy Markdown

@espetro is attempting to deploy a commit to the Open Circle Team on Vercel.

A member of the Team first needs to authorize it.

@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. enhancement New feature or request labels Jun 14, 2026
@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7a904ac1-5af6-4b3d-8337-25f97dad090b

📥 Commits

Reviewing files that changed from the base of the PR and between 4b82b39 and eea4c1c.

📒 Files selected for processing (2)
  • packages/core/package.json
  • packages/methods/package.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/core/package.json
  • packages/methods/package.json

Walkthrough

This PR adds React Native as a supported framework target across the formisch monorepo. A new file packages/core/src/framework/index.react-native.ts is introduced, implementing a self-contained reactive signal system: createSignal with dependency tracking, batch for deferred bulk notifications, untrack for dependency-free execution, setListener for registering the active listener, and createId for unique ID generation. The Framework type union in packages/core/src/framework/index.ts is extended with 'react-native'. Both packages/core and packages/methods receive corresponding defineFrameworkConfig('react-native') entries in their tsdown build configs and ./react-native subpath exports in their package.json files.

Suggested reviewers

  • fabian-hiller
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding a React Native framework adapter to both core and methods packages.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the React Native adapter implementation, file changes, and verification results.
Linked Issues check ✅ Passed All coding requirements from #117 are met: Framework union extended, react-native adapter implemented, exports added to both packages, and build matrices updated.
Out of Scope Changes check ✅ Passed All changes are in scope. The PR correctly focuses on the signal-layer foundation only, with component adapters explicitly deferred to follow-up work.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/framework/index.react-native.ts`:
- Line 56: The createSignal function is missing the zero-argument overload that
exists in the base framework contract. In both index.react-native.ts and
index.react.ts, add an overload declaration for createSignal that accepts no
parameters and returns Signal<T>, in addition to the existing overload that
takes an initial value parameter. This ensures both adapters match the complete
API contract defined in the base framework, allowing users to call
createSignal() with no arguments as well as with an initial value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3797d810-21ca-48d1-99fc-c874f64b76c3

📥 Commits

Reviewing files that changed from the base of the PR and between be7c39a and 4b82b39.

📒 Files selected for processing (6)
  • packages/core/package.json
  • packages/core/src/framework/index.react-native.ts
  • packages/core/src/framework/index.ts
  • packages/core/tsdown.config.ts
  • packages/methods/package.json
  • packages/methods/tsdown.config.ts

* @returns The created signal.
*/
// @__NO_SIDE_EFFECTS__
export function createSignal<T>(value: T): Signal<T> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify createSignal signature consistency across framework entrypoints and call sites.
fd -i 'index*.ts' packages/core/src/framework
rg -nP --type=ts '^export function createSignal<.*>\(\): Signal<.*>;' packages/core/src/framework -C2
rg -nP --type=ts '^export function createSignal<.*>\(value: .*' packages/core/src/framework -C2
rg -nP --type=ts '\bcreateSignal\s*(<[^>]+>)?\s*\(\s*\)' packages -C2

Repository: open-circle/formisch

Length of output: 2059


createSignal is missing the zero-arg overload from the base framework API.

The base contract in packages/core/src/framework/index.ts declares two overloads for createSignal—one without arguments and one with an initial value. At line 56 in the react-native adapter, only the required-argument overload is exposed, creating an API mismatch. The same issue exists in index.react.ts.

Suggested fix
 // `@__NO_SIDE_EFFECTS__`
+export function createSignal<T>(): Signal<T | undefined>;
 export function createSignal<T>(value: T): Signal<T>;
-export function createSignal<T>(value: T): Signal<T | undefined> {
+export function createSignal<T>(value?: T): Signal<T | undefined> {
   const subscribers = new Set<Listener>();
   return {
     get value() {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/framework/index.react-native.ts` at line 56, The
createSignal function is missing the zero-argument overload that exists in the
base framework contract. In both index.react-native.ts and index.react.ts, add
an overload declaration for createSignal that accepts no parameters and returns
Signal<T>, in addition to the existing overload that takes an initial value
parameter. This ensures both adapters match the complete API contract defined in
the base framework, allowing users to call createSignal() with no arguments as
well as with an initial value.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 6 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/core/src/framework/index.react-native.ts">

<violation number="1" location="packages/core/src/framework/index.react-native.ts:7">
P2: React Native adapter duplicates the entire React adapter signal implementation. The two files differ only by the `framework` constant, creating a maintenance burden and drift risk for any future signal-layer changes.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

@@ -0,0 +1,131 @@
import type { Signal } from '../types/signal/index.ts';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: React Native adapter duplicates the entire React adapter signal implementation. The two files differ only by the framework constant, creating a maintenance burden and drift risk for any future signal-layer changes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/src/framework/index.react-native.ts, line 7:

<comment>React Native adapter duplicates the entire React adapter signal implementation. The two files differ only by the `framework` constant, creating a maintenance burden and drift risk for any future signal-layer changes.</comment>

<file context>
@@ -0,0 +1,131 @@
+/**
+ * The current framework being used.
+ */
+export const framework: Framework = 'react-native';
+
+/**
</file context>

@fabian-hiller fabian-hiller left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for this PR! It would be great to offer Formisch for React Native within the next weeks.

I think we should do what you marked as "Out of scope" in the PR description as part of this PR. Since React Native is a special case it would be great to confirm that what we have in mind make sense and works with React Native. A bigger PR is ok in such a case. I just want to prevent merging something that can not be verified and tested.

Maybe I am wrong but there might be a chance that the only change we need is setting the FieldElement type to e.g. React Native's TextInput, Switch, and so on. FieldElement is only used in InternalBaseStore and getElementInput. Since getElementInput is not used anywhere else within the core and methods package, it can probably be ignored.

After that, we need a new frameworks/react-native and playgrounds/react-native folder with React Native specific code.

/**
* The current framework being used.
*/
export const framework: Framework = 'react-native';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this file is fully identically to index.react.ts we could try re-exporting the functions of index.react.ts so that we only have to maintain one implementation. Just a spontaneous idea we should at least test and consider. But this is not a blocker.

@espetro espetro Jun 15, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll check it, nevertheless take into account that the development teams at react and react-native don't sync on their releases, and this could create issues in the future. For instance, the React team announced the v19 release in Dec. 2024 and it took the React Native team until Feb. 2025 to ship it. That's 3 months worth of potential bugs (now with AI-assisted development it'd be much less of course, but it still implies some risk)

@fabian-hiller fabian-hiller self-assigned this Jun 14, 2026
@espetro

espetro commented Jun 15, 2026

Copy link
Copy Markdown
Author

Hey @fabian-hiller before I proceed with the changes, I want to get your approval on one topic: when looking how to decouple core types from DOM types, I noticed the easiest fix was to change InternalBaseStore.elements and initialElements from FieldElement[] to object[] at packages/core/src/types/field/field.ts, however that's not helpful at all from a typesafe perspective.

Thus, instead I'd like to suggest a minimal structural base type:

At packages/core/src/types/field/field.ts

export interface FieldElementBase {
  readonly isConnected?: boolean;
}

InternalBaseStore.elements / .initialElements become FieldElementBase[]. The DOM FieldElement union is untouched; it still satisfies FieldElementBase structurally because all three HTML element types inherit isConnected: boolean from Node. No existing code breaks.

At frameworks/react/src/hooks/useField/useField.ts

The cleanup effect currently reads:

internalFieldStore.elements = internalFieldStore.elements.filter(
  (element) => element.isConnected
);

Because elements is now FieldElementBase[] (where isConnected is boolean | undefined), the filter needs a cast to keep TypeScript happy:

internalFieldStore.elements = internalFieldStore.elements.filter(
  (element) => (element as FieldElement).isConnected
);

That's the only change to the React adapter.

Other frameworks

All other frameworks (Preact, Qwik, Solid, Svelte, Vue) reference FieldElement only in their own framework-layer types, not in InternalBaseStore. They are unaffected.

Why not extends HTMLElement?

HTMLSelectElement does not extend HTMLInputElement, and forcing a common DOM supertype would drag web-only APIs into the core contract. The FieldElementBase approach (inspired by react-hook-form's CustomElement<T> pattern) keeps the core contract minimal: the only thing the core actually reads from entries in elements is isConnected (for cleanup filtering).


That's the full footprint outside the frameworks/react-native/ and playgrounds/react-native/ directories.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add React Native framework adapter

2 participants