Skip to content

Add react implementation of state management#124

Open
andrew-polk wants to merge 1 commit intoui-controllerfrom
state-management-react
Open

Add react implementation of state management#124
andrew-polk wants to merge 1 commit intoui-controllerfrom
state-management-react

Conversation

@andrew-polk
Copy link
Contributor

@andrew-polk andrew-polk commented Dec 10, 2025

This change is Reviewable


Open with Devin

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a React adapter for the @ethnolib/state-management-core library, enabling React applications to use the framework-agnostic state management system. The implementation follows the pattern established by the existing Svelte adapter, providing a useField hook that binds Field objects to React state.

Key Changes

  • Created new @ethnolib/state-management-react package with the useField hook
  • Added build configuration (Vite, TypeScript, Vitest) following the existing pattern
  • Included comprehensive README documentation explaining usage patterns

Reviewed changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
package-lock.json Added dependencies for the new React state management package and updated Svelte package references
components/state-management/state-management-react/package.json Package manifest defining the React adapter with build scripts and dev dependencies
components/state-management/state-management-react/index.ts Main entry point exporting the useField hook
components/state-management/state-management-react/src/use-field.ts Core implementation of the React hook for binding Field objects to React state
components/state-management/state-management-react/vite.config.ts Vite build configuration for library mode
components/state-management/state-management-react/vitest.config.ts Vitest test configuration
components/state-management/state-management-react/tsconfig.json Base TypeScript configuration with React JSX support
components/state-management/state-management-react/tsconfig.lib.json Library-specific TypeScript configuration for build output
components/state-management/state-management-react/README.md Documentation explaining the state management system and usage patterns

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

"@vitejs/plugin-react-swc": "^3.8.0",
"tsx": "^4.19.2",
"typescript": "^5.2.2"
},
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

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

Missing peerDependencies for "react" (and optionally "react-dom"). Since this package is a React adapter that imports from "react", it should declare React as a peer dependency to ensure consumers have it installed. Consider adding:

"peerDependencies": {
  "react": "^17.0.0 || ^18.0.0"
}
Suggested change
},
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0"
},

Copilot uses AI. Check for mistakes.
_setFieldValue(value);
}

field.updateUI = (value) => _setFieldValue(value);
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

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

The field.updateUI assignment on every render can cause issues. If the component re-renders (e.g., due to parent state changes), this assignment will overwrite the callback, potentially breaking synchronization. Consider using useEffect to set this up once:

useEffect(() => {
  field.updateUI = (value) => _setFieldValue(value);
  return () => {
    field.updateUI = null; // cleanup
  };
}, [field]);

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +15
export function useField<T>(field: Field<T>): [T, (value: T) => void] {
const [fieldValue, _setFieldValue] = useState(field.value);

function setFieldValue(value: T) {
field.requestUpdate(value);
_setFieldValue(value);
}

field.updateUI = (value) => _setFieldValue(value);

return [fieldValue, setFieldValue];
}
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

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

The useField hook lacks test coverage. The sibling package state-management-svelte has tests (e.g., transform-view-model.test.ts) for similar functionality. Consider adding tests to verify:

  • Initial state matches field value
  • Updates through setFieldValue call field.requestUpdate
  • Updates through field.updateUI update React state
  • Cleanup of field.updateUI on unmount

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,108 @@
# State Management Core

This package provides a simple interface for working with multiple reactive framworks.
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

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

Typo: "framworks" should be "frameworks"

Suggested change
This package provides a simple interface for working with multiple reactive framworks.
This package provides a simple interface for working with multiple reactive frameworks.

Copilot uses AI. Check for mistakes.
const person = usePersonViewModel();

// Replace `defineReactiveState` with your framework's mechanism
const reactiveName = defineReactiveSate();
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

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

Typo: "defineReactiveSate" should be "defineReactiveState"

Suggested change
const reactiveName = defineReactiveSate();
const reactiveName = defineReactiveState();

Copilot uses AI. Check for mistakes.
export default defineConfig({
root: __dirname,
cacheDir:
"../../../node_modules/.vite/components/state-management/state-management-core",
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

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

The cacheDir path references "state-management-core" but should reference "state-management-react" to match the actual package being built.

Suggested change
"../../../node_modules/.vite/components/state-management/state-management-core",
"../../../node_modules/.vite/components/state-management/state-management-react",

Copilot uses AI. Check for mistakes.
},
lib: {
entry: "./index.ts",
name: "@ethnolib/find-language",
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

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

The library name is incorrectly set to "@ethnolib/find-language". It should be "@ethnolib/state-management-react" to match the package name.

Suggested change
name: "@ethnolib/find-language",
name: "@ethnolib/state-management-react",

Copilot uses AI. Check for mistakes.
"declarationMap": true
},
"exclude": [
"langtagProcessing.ts",
Copy link

Copilot AI Dec 10, 2025

Choose a reason for hiding this comment

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

The exclude pattern references "langtagProcessing.ts" which doesn't exist in this package. This appears to be a leftover from a template and should be removed.

Suggested change
"langtagProcessing.ts",

Copilot uses AI. Check for mistakes.
Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review

_setFieldValue(value);
}

field.updateUI = (value) => _setFieldValue(value);

Choose a reason for hiding this comment

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

🔴 Missing useEffect for field.updateUI assignment causes stale callback after unmount

The field.updateUI callback is assigned directly during render (line 12) rather than inside a useEffect. This means:

  1. No cleanup on unmount: When the component using useField unmounts, field.updateUI still references the stale _setFieldValue from the unmounted component. If anything later sets field.value (which triggers updateUI via the setter at components/state-management/state-management-core/src/field.ts:42), it will call setState on an unmounted component.

  2. Side effect during render: Assigning field.updateUI mutates an external object during the render phase, violating React's rules about pure rendering.

Detailed explanation and comparison with Svelte adapter

The Svelte adapter (components/state-management/state-management-svelte/src/field.svelte.ts:9) sets field.updateUI in the constructor, which is appropriate for Svelte's lifecycle. However, in React, side effects that interact with external state must be placed in useEffect with proper cleanup.

The fix should wrap the assignment in useEffect and return a cleanup function:

useEffect(() => {
  field.updateUI = (value) => _setFieldValue(value);
  return () => { field.updateUI = null; };
}, [field]);

Without this, if a component unmounts and the Field object persists (which is the expected pattern — Fields live in view models that outlive individual React components), subsequent field.value = x calls will attempt to update React state on an unmounted component. While React 18+ silently ignores this, it indicates a real resource leak where the Field holds a reference to a dead component's state setter.

Prompt for agents
In components/state-management/state-management-react/src/use-field.ts, the field.updateUI assignment on line 12 should be moved into a useEffect hook with proper cleanup. Add useEffect to the import from react on line 1, then replace line 12 with:

useEffect(() => {
  field.updateUI = (value) => _setFieldValue(value);
  return () => { field.updateUI = null; };
}, [field]);

This ensures the callback is properly set up after mount and cleaned up on unmount, preventing stale references to the setState function of an unmounted component.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +7 to +10
function setFieldValue(value: T) {
field.requestUpdate(value);
_setFieldValue(value);
}

Choose a reason for hiding this comment

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

🔴 setFieldValue calls _setFieldValue redundantly, risking state desync if onUpdateRequested modifies the field

In setFieldValue, _setFieldValue(value) on line 9 is called after field.requestUpdate(value) on line 8. However, requestUpdate already triggers _setFieldValue(value) via the updateUI callback (see components/state-management/state-management-core/src/field.ts:23-28requestUpdate sets this.value, whose setter at line 42 calls this.updateUI(value)).

How this causes state desync

The call chain when setFieldValue(value) is invoked:

  1. field.requestUpdate(value)this.value = valuethis.updateUI(value)_setFieldValue(value)
  2. _onUpdateRequested(value, oldValue) runs — if this callback modifies field.value to a different value (e.g., normalization/validation), that triggers updateUI(correctedValue)_setFieldValue(correctedValue)
  3. Back in setFieldValue, line 9 calls _setFieldValue(value) with the original value, overwriting the corrected value from step 2.

While current usages of _onUpdateRequested in the codebase don't modify the same field they're attached to, this is a latent bug that will manifest as soon as any onUpdateRequested callback normalizes or validates the field's own value. The Svelte adapter (components/state-management/state-management-svelte/src/field.svelte.ts:19-22) does not have this issue because it sets _state before calling requestUpdate, and the redundant updateUI call sets _state to the same value.

The fix is to remove line 9 (_setFieldValue(value)) since requestUpdate already triggers the UI update via updateUI.

Suggested change
function setFieldValue(value: T) {
field.requestUpdate(value);
_setFieldValue(value);
}
function setFieldValue(value: T) {
field.requestUpdate(value);
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants