// Step-by-step guide for migrating React applications to Gea — covering project setup, component conversion, state management, routing, styling, and known pitfalls. Use when converting an existing React codebase to Gea or when advising on migration strategy.
Step-by-step guide for migrating React applications to Gea — covering project setup, component conversion, state management, routing, styling, and known pitfalls. Use when converting an existing React codebase to Gea or when advising on migration strategy.
Migrating React Codebases to Gea
This skill documents a battle-tested process for converting React applications to the Gea framework, based on a full migration of the oldboyxx/jira_clone — a non-trivial React app with routing, state management, styled-components, modals, and drag-and-drop.
Read reference.md in this skill directory for the complete conversion reference with side-by-side code examples.
Prerequisites
Before starting, read the gea-framework skill (skills/gea-framework/SKILL.md) to understand Gea's core concepts: Stores, Components, JSX rules, and the Router.
Also read the gea-ui-components skill (skills/gea-ui-components/SKILL.md) — the Jira clone uses @geajs/ui for Dialog, Button, Select, Avatar, Toaster, and Link. Most React apps have custom or third-party versions of these; switching to @geajs/ui eliminates significant migration work.
Migration Strategy
Phase 1: Scaffold the Gea project
Create a new directory alongside the React app (e.g. jira_clone_gea/).
Set up package.json, vite.config.ts, index.html, and src/main.ts.
Install @geajs/core, @geajs/vite-plugin, and @geajs/ui.
Copy over static assets (fonts, icons, images) and global CSS variables.
Optionally configure Tailwind CSS (the Jira clone uses it, but plain CSS works equally well).
Phase 2: Convert the data layer first
Convert stores and API utilities before touching any UI. This gives you a working data layer to test components against.
Stores — Convert each React state container (Context, Redux slices, useState/useMergeState hooks) into a Gea Store class.
Toast store — Create a thin adapter over @geajs/ui's ToastStore so call sites use a familiar toastStore.success(msg) / toastStore.error(err) API.
Auth flow — Move authentication from a route-level useEffect into App.created().
Validation utilities — Port form validation helpers (if any) as plain functions.
Phase 3: Convert components top-down
Start with the root App component and work down the component tree:
App → class component with created() for initialization
Layout shell (sidebar, navbar) → class components reading from stores
Page views (Board, Settings) → class components with template()
Modals / dialogs → replace custom modal components with @geajs/uiDialog
Forms → replace custom selects with @geajs/uiSelect, buttons with @geajs/uiButton
Presentational components (Avatar, Icon, Spinner) → function components or @geajs/ui equivalents
Phase 4: Port styling
Convert styled-components (or CSS-in-JS) to plain CSS. Use CSS variables for design tokens.
Phase 5: Wire up routing
Replace react-router-dom with Gea's built-in router. Use matchRoute for URL-driven modals.
Phase 6: Test and iterate
Compare both apps side-by-side, pixel by pixel. Fix visual discrepancies by inspecting the React app's computed styles and replicating exact values.
Conversion Rules
Components
React
Gea
function MyComponent() {} with hooks
class MyComponent extends Component {} with member variables
function MyComponent({ props }) (stateless)
export default function MyComponent({ props })
useState(initial)
Member variable: myField = initial
useEffect(() => {}, [])
created() lifecycle method
useEffect(() => { return cleanup }, [])
created() + dispose()
useRef() for DOM
ref={this.myElement} on the element, or this.el for the root
Guards on nested groups stack parent → child. The parent guard runs first; the child guard only runs if the parent passes.
Guards are intentionally synchronous — they check store state, not async APIs. For async checks (API calls, fetching data), use created() in the component.
Styling
React apps commonly use styled-components or CSS-in-JS. Gea uses plain CSS with class attributes (optionally with Tailwind).
Conversion process:
Open each styled-component definition (e.g. Styles.js).
Extract every CSS property and value.
Create equivalent CSS rules in a stylesheet.
Replace styled component usage with <div class="my-class">.
For dynamic styles, use template literal classes: class={`btn ${active ? 'active' : ''}`}
For truly dynamic values (computed sizes, positions), use inline style — either a string (style={`width:${size}px`}) or a style object (style={{ width: size + 'px' }}). Gea supports React-style camelCase style objects.
Event Handlers
React
Gea
Notes
onClick={fn}
click={fn}
Both native and React-style names work
onChange={fn} on <input type="text">
input={fn}
input fires on every keystroke; change fires on blur
onChange={fn} on <select>
change={fn}
onChange={fn} on <input type="checkbox">
change={fn}
Use with checked={bool}
onBlur={fn}
blur={fn}
onFocus={fn}
focus={fn}
onKeyDown={fn}
keydown={fn}
onSubmit={fn}
submit={fn}
onDoubleClick={fn}
dblclick={fn}
onDragStart={fn}
dragstart={fn}
Native HTML5 drag-and-drop
onDragEnd={fn}
dragend={fn}
onDragOver={fn}
dragover={fn}
onDragLeave={fn}
dragleave={fn}
onDrop={fn}
drop={fn}
Hooks → Gea Equivalents
React Hook
Gea Equivalent
useState
Member variable (this.myField = value)
useEffect(fn, [])
created() lifecycle
useEffect(fn, [dep])
Read dep in template() — compiler creates observer automatically
useEffect(() => () => cleanup)
dispose() lifecycle (call super.dispose() if overriding)
useRef
ref={this.myEl} for specific elements; this.el for root; member variable for mutable refs
The guard shows PageLoader until auth and data loading complete. App.created() handles the async work, then router.replace(router.path) re-triggers route resolution so the guard re-evaluates and passes. Routes are set in App.tsx (not router.ts) to avoid circular dependencies — views import router, so router.ts must not import views.
Modals with @geajs/ui Dialog
React typically uses a Modal component with render props and portal:
Gea uses @geajs/uiDialog with open and onOpenChange props:
import { Dialog } from'@geajs/ui'// Route-driven dialog (opens based on URL)
{this.showIssueDetail && (
<Dialogopen={true}onOpenChange={(d:any) => {
if (!d.open) this.closeIssueDetail()
}}
class="dialog-issue-detail"
>
<IssueDetailsissueId={this.issueId}onClose={() => this.closeIssueDetail()} />
</Dialog>
)}
// State-driven dialog (opens based on component state)
{this.searchModalOpen && (
<Dialogopen={true}onOpenChange={(d:any) => {
if (!d.open) this.closeSearchModal()
}}
class="dialog-search"
>
<IssueSearchonClose={() => this.closeSearchModal()} />
</Dialog>
)}
Key Dialog patterns:
Controlled open state: Pass open={true} and conditionally render the Dialog.
Close via onOpenChange: Listen for {open: false} to trigger cleanup.
Route-driven dialogs: Use router.params to derive open state from the URL. Close by navigating away.
State-driven dialogs: Use a boolean member variable (e.g. this.searchModalOpen) to toggle.
Nested dialogs: Dialogs inside other components (e.g. time tracking dialog inside IssueDetails) work fine.
Layout Components with page Prop
React uses <Route> components and useRouteMatch for view switching. Gea uses layouts in the route config — the router resolves the child component and passes it as a page prop: