Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,16 @@ A reproducible example from an online editor such as CodeSandbox or StackBlitz

**Desktop (please complete the following information):**

- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]

**Smartphone (please complete the following information):**

- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]

**Additional context**
Add any other context about the problem here.
14 changes: 7 additions & 7 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ _Use this section to describe how you tested your changes; if you haven't includ

## Checklist:

- [ ] I have completed the above PR template
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation (if applicable)
- [ ] My changes generate no new warnings
- [ ] My changes include tests that prove my fix is effective (or that my feature works as intended)
- [ ] New and existing unit/integration tests pass locally with my changes
- [ ] Any dependent changes have corresponding PRs that are listed above
- [ ] I have completed the above PR template
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation (if applicable)
- [ ] My changes generate no new warnings
- [ ] My changes include tests that prove my fix is effective (or that my feature works as intended)
- [ ] New and existing unit/integration tests pass locally with my changes
- [ ] Any dependent changes have corresponding PRs that are listed above
61 changes: 61 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# @formio/react

## Identity

- **Path:** `packages/react`. **Published name:** `@formio/react`, version in [`package.json`](./package.json).
- **License:** MIT. **OSS sync:** YES — `ossRepo: { repo: github.com/formio/react, srcPath: "." }`. Everything in `src/` ships publicly.
- **Module/language:** TypeScript (CJS `main: lib/index.js`), mixed era. Targets React 17–19 (`peerDependencies: react/react-dom >=17`; `@types/react ^19`).
- **Purpose:** the React wrapper around the `@formio/js` renderer + `@formio/core` data engine. It owns the React surface (components, context, hooks) and the **lifecycle bridge** from React's re-render model to the imperative `@formio/js` instance — **not** rendering/validation/submission, which live in `@formio/js`.

## Floor — immutable musts

- **Never widen a create-effect's dependency array to react to a data prop.** Anything in `Form.tsx`'s `[formConstructor, formReadyCallback, formSource, options, url]` or `FormBuilder.tsx`'s `[Builder, initialForm, onBuilderReady, options]` triggers a full instance destroy+recreate. Apply data changes (`submission`) to the live instance in a separate `isEqual`-guarded effect instead. See [react/create-effect-deps-rebuild-02](../../docs/gotchas/react.md#reactcreate-effect-deps-rebuild-02--the-create-effect-dependency-array-rebuilds-the-whole-instance) (markers `G-RC03`/`G-RC04`).
- **Guard every `await instance.ready` against mount/unmount/redraw.** Keep the `isMounted` ref check after the await and destroy orphaned/pending instances (`G-RC01`/`G-RC02`). After `destroy(true)`, instance fields are _deleted_ — optional-chain in any continuation. See [react/async-create-race-01](../../docs/gotchas/react.md#reactasync-create-race-01--async-instance-creation-races-react-mountunmountredraw).
- **Clone shared default props before handing them to a builder/form.** Module-level `const DEFAULT_X = {}` defaults are a single shared reference; the builder mutates the form definition in place. Use `fastCloneDeep`. See [react/shared-mutable-default-props-03](../../docs/gotchas/react.md#reactshared-mutable-default-props-03--module-level-default-props-are-shared-and-the-builder-mutates-them-in-place) (marker `G-RC05`).
- **`form`/`forms` and `submission`/`submissions` are different Redux modules** (single-entity CRUD vs list+pagination). They're legacy `.js`; TS won't catch a mix-up. See [react/singular-plural-redux-modules-04](../../docs/gotchas/react.md#reactsingular-plural-redux-modules-04--formforms-and-submissionsubmissions-are-different-modules).
- **Keep nothing secret/license-gated in source** and keep `GOTCHA` markers opaque — `src/` is published to the public OSS repo.
- **Match the file's era.** New components/hooks → `.tsx`/`.ts`, function components, hooks. Preserve the legacy `.js` modules (`src/modules/`, `types.js`, `utils.js`) and the deprecated `ReactComponent.jsx` — don't TS-convert them. Adds to [`/STANDARDS.md`](../../STANDARDS.md); never overrides it.

## Ceiling — emerging patterns

- **Pattern: a wrapper component holds the instance in `useState`, a `renderElement` ref, and an `isMounted` ref; the create-effect `await`s `.ready`, guards liveness, destroys the previous instance, then `setInstance`. Example:** [`src/components/Form.tsx`](./src/components/Form.tsx) (lines 252–376) — the smallest reference bridge.
- **Pattern: data props are applied to the live instance in their own `isEqual`-guarded effect, never via the create-effect deps. Example:** [`src/components/Form.tsx`](./src/components/Form.tsx) submission-sync effect (lines 367–373).
- **Pattern: on redraw, destroy the not-yet-ready previous instance before creating the replacement via a `pendingBuilder` ref. Example:** [`src/components/FormBuilder.tsx`](./src/components/FormBuilder.tsx) (lines 189–209, marker `G-RC02`).

## Blast radius

**4 dependents, tier: medium** (`@formio/enterprise-form-builder-react`, `oss-portal`, `react-efb-demo`, `react-playground`). See [`/docs/dependencies/react.md`](../../docs/dependencies/react.md).

## Test & build

```sh
pnpm -F @formio/react build # tsc --project tsconfig.json → lib/
pnpm -F @formio/react test # jest (jsdom + Testing Library) — REAL runner
pnpm -F @formio/react lint # eslint src — REAL
pnpm -F @formio/react check-types # tsc --noEmit
```

- **The runner is `jest`, not mocha.** Root CLAUDE.md says "Mocha is primary" — that does **not** apply here; `test`/`lint` are real commands that run real tests (unlike `@formio/angular`/`@formio/bootstrap`, whose scripts are stubs).
- **"Green"** = `test` + `lint` + `check-types`. For any lifecycle change, behavioral green also means exercising the EFB + demo apps (`react-efb-demo`, `react-playground`, `oss-portal` in production mode) — that's where redraw/StrictMode bugs surface.
- Single test file: `cd packages/react && pnpm jest src/components/__tests__/<name>.test.tsx`.

## Hot paths & gotchas

See [`/docs/gotchas/react.md`](../../docs/gotchas/react.md). Entries: `react/async-create-race-01`, `react/create-effect-deps-rebuild-02`, `react/shared-mutable-default-props-03`, `react/singular-plural-redux-modules-04`.

## Cross-cutting triggers

- **Touching any create/redraw lifecycle** (create-effect, `isMounted`/`pendingBuilder` refs, instance recreation in `Form.tsx`/`FormBuilder.tsx`/`FormEdit.tsx`) → read [`/docs/cross-cutting/framework-wrapper-lifecycle.md`](../../docs/cross-cutting/framework-wrapper-lifecycle.md). This is the `@formio/js ↔ @formio/react` create/redraw contract (FIO-11128, FIO-11245, FIO-9944).
- **Touching `destroy()` / teardown / async continuations** → read [`/docs/cross-cutting/component-lifecycle.md`](../../docs/cross-cutting/component-lifecycle.md). After `destroy(true)`, instance fields are deleted; this is a 6-package contract (FIO-10902).
- **Any rendering/validation/submission behavior question** → it's almost certainly `@formio/js`, not here. Read [`/docs/architecture/formio.js.md`](../../docs/architecture/formio.js.md) and [`/docs/gotchas/formio.js.md`](../../docs/gotchas/formio.js.md) first.
- **Adding a new `@formio/js` import** → use a declared `exports` subpath only (`@formio/js` or `@formio/js/utils`); never `@formio/js/lib/...`. See [`/docs/dependencies/react.md`](../../docs/dependencies/react.md#formiojs-subpath-rule).
- **Changing a `FormBuilder`/`Form` callback signature** (`onSaveComponent`, `onDeleteComponent`, `onChange`, …) → these are a shared contract with `@formio/enterprise-form-builder-react`, which threads them into its conflict-change tracking. FIO-10763 widened `onSaveComponent` (`path`, `isNew`) across both packages + needed two changesets. See the EFB-wrapper-stack row in [`/docs/cross-cutting/README.md`](../../docs/cross-cutting/README.md).
- **`major` bump** → coordinate the OSS release (ships to `github.com/formio/react`); the planned major removes `ReactComponent`.

## References

- Repo-wide: [`/CLAUDE.md`](../../CLAUDE.md), [`/STANDARDS.md`](../../STANDARDS.md)
- Architecture: [`/docs/architecture/react.md`](../../docs/architecture/react.md)
- Dependencies: [`/docs/dependencies/react.md`](../../docs/dependencies/react.md)
- Gotchas: [`/docs/gotchas/react.md`](../../docs/gotchas/react.md)
- Renderer it wraps: [`/docs/architecture/formio.js.md`](../../docs/architecture/formio.js.md)
32 changes: 16 additions & 16 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

Thank you for helping this project become a better library :)

- [Triage](#triage)
- [Reporting Bugs](#reporting-bugs)
- [Providing a feature request](#providing-a-feature-request)
- [Pull requests](#pull-requests)
- [Triage](#triage)
- [Reporting Bugs](#reporting-bugs)
- [Providing a feature request](#providing-a-feature-request)
- [Pull requests](#pull-requests)

## Triage

Expand All @@ -23,21 +23,21 @@ Open an issue, making sure to follow the Feature request template.

### Getting started

- If there isn't one already, open an issue describing the bug or feature request that you are going to solve in your pull request.
- Create a fork of react-native-maps
- If you already have a fork, make sure it is up to date
- Git clone your fork and run `yarn` in the base of the cloned repo to setup your local development environment.
- Create a branch from the master branch to start working on your changes.
- If there isn't one already, open an issue describing the bug or feature request that you are going to solve in your pull request.
- Create a fork of react-native-maps
- If you already have a fork, make sure it is up to date
- Git clone your fork and run `yarn` in the base of the cloned repo to setup your local development environment.
- Create a branch from the master branch to start working on your changes.

### Committing

- When you made your changes, run `yarn lint` & `yarn test` to make sure the code you introduced doesn't cause any obvious issues.
- When you are ready to commit your changes, use the [Github conventional commits](https://gist.github.com/qoomon/5dfcdf8eec66a051ecd85625518cfd13) convention for you commit messages, as we use your commits when releasing new versions.
- Use present tense: "add awesome component" not "added awesome component"
- Limit the first line of the commit message to 100 characters
- Reference issues and pull requests before committing
- When you made your changes, run `yarn lint` & `yarn test` to make sure the code you introduced doesn't cause any obvious issues.
- When you are ready to commit your changes, use the [Github conventional commits](https://gist.github.com/qoomon/5dfcdf8eec66a051ecd85625518cfd13) convention for you commit messages, as we use your commits when releasing new versions.
- Use present tense: "add awesome component" not "added awesome component"
- Limit the first line of the commit message to 100 characters
- Reference issues and pull requests before committing

### Creating the pull request

- The title of the PR needs to follow the same conventions as your commit messages, as it might be used in case of a squash merge.
- Create the pull request against the beta branch.
- The title of the PR needs to follow the same conventions as your commit messages, as it might be used in case of a squash merge.
- Create the pull request against the beta branch.
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

A React library for rendering out forms based on the [Form.io](https://www.form.io) platform.

Official Documentation
--------------------------
## Official Documentation

For the latest documentation, release information, and guides, always refer to the official Form.io Help Documentation available here:

**[https://help.form.io](https://help.form.io/dev/javascript-development/frameworks#react)**
Expand Down Expand Up @@ -324,7 +324,7 @@ A React component wrapper around [a Form.io form](https://help.form.io/dev/form-
| `onCustomEvent` | `(event: { type: string; component: Component; data: JSON; event?: Event; }) => void` | | A callback function that is triggered from a button component configured with "Event" type. |
| `onPrevPage` | `(page: number, submission: JSON) => void` | | A callback function for Wizard forms that gets called when the "Previous" button is pressed. |
| `onNextPage` | `(page: number, submission: JSON) => void` | | A callback function for Wizard forms that gets called when the "Next" button is pressed. |
| `otherEvents` | `[event: string]: (...args: any[]) => void;` | | A "catch-all" prop for subscribing to other events (for a complete list, see [our documentation](https://help.form.io/dev/form-development/form-renderer#form-events)). |
| `otherEvents` | `[event: string]: (...args: any[]) => void;` | | A "catch-all" prop for subscribing to other events (for a complete list, see [our documentation](https://help.form.io/dev/form-development/form-renderer#form-events)). |

#### Examples

Expand Down Expand Up @@ -490,7 +490,7 @@ A React component wrapper around [a Form.io form builder](https://help.form.io/d
| Name | Type | Default | Description |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `initialForm` | `FormType` | | The JSON form definition of the initial form to be rendered in the builder. Oftentimes, this must be a stable reference; otherwise it may destroy and recreate the underlying builder instance and cause unexpected behavior. |
| `options` | `FormBuilderOptions` | | The form builder options. See [here](https://help.form.io/dev/form-development/form-builder#form-builder-options) for more details. |
| `options` | `FormBuilderOptions` | | The form builder options. See [here](https://help.form.io/dev/form-development/form-builder#form-builder-options) for more details. |
| `onBuilderReady` | `(instance: FormBuilder) => void` | | A callback function that gets called when the form builder has rendered. It is useful for accessing the underlying @formio/js FormBuilder instance. |
| `onChange` | `(form: FormType) => void` | | A callback function that gets called when the form being built has changed. |
| `onSaveComponent` | `(component: Component, original: Component, parent: Component, path: string, index: number, isNew: boolean, originalComponentSchema: Component) => void;` | | A callback function that gets called when a component is saved in the builder. |
Expand Down Expand Up @@ -856,7 +856,8 @@ const FormActionButton: FormGridComponentProps['FormActionButton'] = ({
>
<i
className={`${action && action.name === 'Edit' ? 'ri-edit-box-line' : 'ri-delete-bin-line'}`}
></i> {action && action.name === 'Edit' ? 'Edit' : ''}
></i>{' '}
{action && action.name === 'Edit' ? 'Edit' : ''}
</a>
);

Expand Down
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"scripts": {
"test": "jest",
"build": "tsc --project tsconfig.json",
"lint": "eslint src"
"lint": "eslint src",
"check-types": "tsc --noEmit",
"lint:fix": "eslint src --fix"
},
"repository": {
"type": "git",
Expand All @@ -32,8 +34,8 @@
"prop-types": "^15.8.1"
},
"devDependencies": {
"@formio/core": "^2.7.0",
"@formio/js": "^5.4.0",
"@formio/core": "^2.7.1",
"@formio/js": "^5.4.2",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.2",
"@testing-library/react": "^16.1.1",
Expand Down
4 changes: 3 additions & 1 deletion src/components/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ export const Form = (props: FormProps) => {
isMounted.current = true;
return () => {
isMounted.current = false;
}
};
}, []);

useEffect(() => {
Expand Down Expand Up @@ -316,6 +316,7 @@ export const Form = (props: FormProps) => {
);

if (instance) {
// GOTCHA(G-RC01)
if (!isMounted.current) {
instance.destroy(true);
return;
Expand Down Expand Up @@ -345,6 +346,7 @@ export const Form = (props: FormProps) => {
};

createInstance();
// GOTCHA(G-RC03)
}, [formConstructor, formReadyCallback, formSource, options, url]);

useEffect(() => {
Expand Down
Loading
Loading