Dify Component Writing Guide
langgenius/dify
Use when implementing or refactoring React/TypeScript components and the task requires decisions about component ownership, feature boundaries, state, data…
Step-by-step guide for migrating React applications to Gea — covering project setup, component conversion, state management, routing, styling, and known pitfalls.
$ npx skills add dashersw/gea --skill react-to-gea-migration -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install dashersw/gea react-to-gea-migration --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/dashersw/gea.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.cursor/skills/react-to-gea-migration .claude/skills/react-to-gea-migration && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "react-to-gea-migration" agent skill from https://github.com/dashersw/gea/tree/main/.cursor/skills/react-to-gea-migration into .claude/skills/react-to-gea-migration/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "react-to-gea-migration", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/dashersw/gea/tree/main/.cursor/skills/react-to-gea-migrationType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add dashersw/gea --skill react-to-gea-migration -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install dashersw/gea react-to-gea-migration --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/dashersw/gea.git skills-src && mkdir -p .agents/skills && cp -r skills-src/.cursor/skills/react-to-gea-migration .agents/skills/react-to-gea-migration && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "react-to-gea-migration" agent skill from https://github.com/dashersw/gea/tree/main/.cursor/skills/react-to-gea-migration into .agents/skills/react-to-gea-migration/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "react-to-gea-migration", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add dashersw/gea --skill react-to-gea-migration -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install dashersw/gea react-to-gea-migration --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/dashersw/gea.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/.cursor/skills/react-to-gea-migration .cursor/skills/react-to-gea-migration && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "react-to-gea-migration" agent skill from https://github.com/dashersw/gea/tree/main/.cursor/skills/react-to-gea-migration into .cursor/skills/react-to-gea-migration/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "react-to-gea-migration", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/dashersw/gea.git --path .cursor/skills/react-to-gea-migration--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add dashersw/gea --skill react-to-gea-migration -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install dashersw/gea react-to-gea-migration --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/dashersw/gea.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/.cursor/skills/react-to-gea-migration .gemini/skills/react-to-gea-migration && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "react-to-gea-migration" agent skill from https://github.com/dashersw/gea/tree/main/.cursor/skills/react-to-gea-migration into .gemini/skills/react-to-gea-migration/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "react-to-gea-migration", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install dashersw/gea react-to-gea-migrationInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add dashersw/gea --skill react-to-gea-migration -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/dashersw/gea.git skills-src && mkdir -p .github/skills && cp -r skills-src/.cursor/skills/react-to-gea-migration .github/skills/react-to-gea-migration && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "react-to-gea-migration" agent skill from https://github.com/dashersw/gea/tree/main/.cursor/skills/react-to-gea-migration into .github/skills/react-to-gea-migration/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "react-to-gea-migration", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add dashersw/gea --skill react-to-gea-migration -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install dashersw/gea react-to-gea-migration --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/dashersw/gea.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/.cursor/skills/react-to-gea-migration .opencode/skills/react-to-gea-migration && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "react-to-gea-migration" agent skill from https://github.com/dashersw/gea/tree/main/.cursor/skills/react-to-gea-migration into .opencode/skills/react-to-gea-migration/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "react-to-gea-migration", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
react-to-gea-migrationStep-by-step guide for migrating React applications to Gea — covering project setup, component conversion, state management, routing, styling, and known pitfalls.
React To Gea Migration is an agent skill from dashersw/gea. 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.
Its SKILL.md is about 8.1k tokens, which your agent loads only when the skill is triggered. The skill folder holds 1 other file (for example `reference.md`).
It sits in Frontend & Design, covering State management. It works with React and Jira. The repository describes itself as: A batteries-included, reactive JavaScript UI framework. No virtual DOM. Compile-time JSX transforms. Proxy-based stores. Surgical DOM patching. The licence is MIT.
6 steps, taken from the step headings in SKILL.md.
Read from SKILL.md and the folder at commit 51da237. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
No scripts in the folder and no shell commands in SKILL.md (its code samples are typescript and javascript).
From the folder's file list and the shell code blocks in SKILL.md.
Links to these hosts (documentation or services it may open):
github.comFrom URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
React To Gea Migration loads about 8.1k tokens when it runs. Until then it costs about 70 tokens; SKILL.md has 1,967 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from dashersw/gea at commit 51da237, republished under its MIT licence (© dashersw). 1,967 words, ~8,144 tokens.
.claude/skills/react-to-gea-migration/SKILL.md (or your agent's skills folder). This skill also uses 1 other file; get the full folder from GitHub.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.
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.
jira_clone_gea/).package.json, vite.config.ts, index.html, and src/main.ts.@geajs/core, @geajs/vite-plugin, and @geajs/ui.Convert stores and API utilities before touching any UI. This gives you a working data layer to test components against.
useState/useMergeState hooks) into a Gea Store class.@geajs/ui's ToastStore so call sites use a familiar toastStore.success(msg) / toastStore.error(err) API.useEffect into App.created().Start with the root App component and work down the component tree:
created() for initializationtemplate()@geajs/ui Dialog@geajs/ui Select, buttons with @geajs/ui Button@geajs/ui equivalentsConvert styled-components (or CSS-in-JS) to plain CSS. Use CSS variables for design tokens.
Replace react-router-dom with Gea's built-in router. Use matchRoute for URL-driven modals.
Compare both apps side-by-side, pixel by pixel. Fix visual discrepancies by inspecting the React app's computed styles and replicating exact values.
| 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 |
useCallback / useMemo | Not needed — use class methods or store getters |
React.Fragment / <>...</> | Not supported — use a wrapper <div> |
className="foo" | class="foo" |
style={{ color: 'red' }} | style={{ color: 'red' }} (same syntax — compiled to CSS string) |
onClick={fn} | click={fn} |
onChange={fn} (on input) | input={fn} (for text) or change={fn} (for checkbox/select) |
onKeyDown={fn} | keydown={fn} |
<div {...props} /> | Works on HTML elements. On a component tag (<Comp {...props} />), pass props individually |
dangerouslySetInnerHTML={{ __html: html }} | Use onAfterRender with el.innerHTML |
children | children prop (works the same) |
Render props renderContent={modal => <Foo />} | Supported — render props compile to component instantiation |
{(data) => <Child />} (function as child) | Not supported — use named render prop attributes instead (compile error) |
propTypes / defaultProps | Not needed — use TypeScript types and default parameter values |
React:
import ReactDOM from 'react-dom'
ReactDOM.render(<App />, document.getElementById('root'))Gea:
import App from './App'
import './styles.css'
const app = new App()
app.render(document.getElementById('app'))React (hooks + context):
const [filters, setFilters] = useState({ searchTerm: '', userIds: [] })
const updateFilter = (key, value) => setFilters(prev => ({ ...prev, [key]: value }))Gea (Store):
class FiltersStore extends Store {
searchTerm = ''
userIds: string[] = []
setSearchTerm(val: string) { this.searchTerm = val }
toggleUserId(id: string) {
const idx = this.userIds.indexOf(id)
if (idx >= 0) this.userIds.splice(idx, 1)
else this.userIds.push(id)
}
}
export default new FiltersStore()Key differences:
this.count++, this.items.push(x), this.items.splice(i, 1). No spread operators or immutable patterns needed.useMemo with class getters.push, pop, splice, sort, reverse, shift, unshift all trigger reactivity.React (react-router-dom v5):
import { useHistory, useRouteMatch, Route, Switch } from 'react-router-dom'
const history = useHistory()
history.push('/board')
<Route path="/board" component={Board} />
<Route path="/settings" component={Settings} />Gea:
// src/router.ts — bare Router, no view imports (avoids circular dependencies)
import { Router } from '@geajs/core'
export const router = new Router()// App.tsx — set routes here where both router and views are available
import { router } from './router'
import Project from './views/Project'
import Board from './views/Board'
router.setRoutes({
'/': '/project/board',
'/project': {
layout: Project,
guard: AuthGuard,
children: {
'/board': Board,
'/settings': ProjectSettings,
},
},
})// App template — render the router's resolved page
template() {
return (
<div class="app">
<Outlet />
</div>
)
}Key differences:
router.ts exports a bare Router instance — no view imports, avoiding circular dependencies (views import router, so router.ts must not import views).router.setRoutes(...) is called in App.tsx where both the router and view components are available.router.push(path) for navigation, router.replace(path) for replacing history.router.path and router.params are reactive — read them in template() or class getters.<Outlet /> to render the resolved page/layout hierarchy.page prop.React uses route-level components or HOCs for auth protection:
<Route path="/dashboard" render={() =>
isAuthenticated ? <Dashboard /> : <Redirect to="/login" />
} />Gea uses guards — synchronous functions on route groups:
import authStore from './stores/auth-store'
import PageLoader from './components/PageLoader'
export const AuthGuard = () => {
if (authStore.isAuthenticated) return true
return PageLoader // show this component instead
// or: return '/login' — redirect to login page
}Apply a guard to a route group to protect all children:
'/project': {
layout: Project,
guard: AuthGuard,
children: {
'/board': Board,
'/settings': ProjectSettings,
},
}Guard return values:
| Return | Effect |
|---|---|
true | Proceed to the route |
string | Redirect to that path |
Component | Render it instead of the route |
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.
React apps commonly use styled-components or CSS-in-JS. Gea uses plain CSS with class attributes (optionally with Tailwind).
Conversion process:
Styles.js).<div class="my-class">.class={`btn ${active ? 'active' : ''}`}style — either a string (style={`width:${size}px`}) or a style object (style={{ width: size + 'px' }}). Gea supports React-style camelCase style objects.| 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} |
| 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 |
useMemo(fn, [deps]) | Store getter or class getter |
useCallback(fn, [deps]) | Class method (stable by default) |
useContext | Import the store singleton directly |
useReducer | Store with methods |
Custom hooks (e.g. useMergeState) | Store class or component methods |
| React Library | Gea Replacement |
|---|---|
react-router-dom | @geajs/core router (Router, Outlet, Link, matchRoute, guards) |
styled-components / CSS-in-JS | Plain CSS + class attributes + inline style for dynamic values |
react-beautiful-dnd | Manual drag-and-drop with native HTML5 drag events (dragstart, dragend, dragover, drop) |
react-modal / custom modals | @geajs/ui Dialog component |
Custom <Select> / react-select | @geajs/ui Select component |
Custom <Button> | @geajs/ui Button component |
react-toastify / custom toasts | @geajs/ui Toaster + ToastStore |
Custom <Avatar> | @geajs/ui Avatar component |
prop-types | TypeScript types |
lodash/xor (for toggle arrays) | Array.indexOf + splice / filter in store methods |
moment / date-fns | Custom utility functions or native Intl.DateTimeFormat |
react-quill / rich text editors | Integrate via onAfterRender + manual DOM management |
React typically uses a route-level Authenticate component with useEffect:
const Authenticate = () => {
const history = useHistory()
useEffect(() => {
if (!getStoredAuthToken()) {
api.post('/authentication/guest').then(({ authToken }) => {
storeAuthToken(authToken)
history.push('/')
})
}
}, [])
return <PageLoader />
}Gea uses a route guard + router.setRoutes in App:
// router.ts — bare Router instance (no view imports to avoid circular deps)
import { Router } from '@geajs/core'
export const router = new Router()// App.tsx — guard + route config + async init
import { router } from './router'
import authStore from './stores/auth-store'
import projectStore from './stores/project-store'
import PageLoader from './components/PageLoader'
import Project from './views/Project'
import Board from './views/Board'
import ProjectSettings from './views/ProjectSettings'
const AuthGuard = () => {
if (authStore.isAuthenticated && !projectStore.isLoading) return true
return PageLoader
}
router.setRoutes({
'/': '/project/board',
'/project': {
layout: Project,
guard: AuthGuard,
children: {
'/board': Board,
'/settings': ProjectSettings,
},
},
})
export default class App extends Component {
async created() {
if (!authStore.isAuthenticated) {
await authStore.authenticate()
} else {
await authStore.fetchCurrentUser()
}
await projectStore.fetchProject()
router.replace(router.path)
}
template() {
return (
<div class="app">
<Outlet />
<Toaster />
</div>
)
}
}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.
@geajs/ui DialogReact typically uses a Modal component with render props and portal:
<Route path="/issues/:id" render={props => (
<Modal isOpen onClose={() => history.push('/board')}
renderContent={modal => <IssueDetails issueId={props.match.params.id} />}
/>
)} />Gea uses @geajs/ui Dialog with open and onOpenChange props:
import { Dialog } from '@geajs/ui'
// Route-driven dialog (opens based on URL)
{this.showIssueDetail && (
<Dialog
open={true}
onOpenChange={(d: any) => {
if (!d.open) this.closeIssueDetail()
}}
class="dialog-issue-detail"
>
<IssueDetails issueId={this.issueId} onClose={() => this.closeIssueDetail()} />
</Dialog>
)}
// State-driven dialog (opens based on component state)
{this.searchModalOpen && (
<Dialog
open={true}
onOpenChange={(d: any) => {
if (!d.open) this.closeSearchModal()
}}
class="dialog-search"
>
<IssueSearch onClose={() => this.closeSearchModal()} />
</Dialog>
)}Key Dialog patterns:
open={true} and conditionally render the Dialog.onOpenChange: Listen for {open: false} to trigger cleanup.router.params to derive open state from the URL. Close by navigating away.this.searchModalOpen) to toggle.page PropReact 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:
// Project is a layout — route config passes the resolved child as `page`
export default class Project extends Component {
get issueId(): string {
return router.params.issueId || ''
}
get showIssueDetail(): boolean {
return !!this.issueId
}
template({ page }: any) {
return (
<div class="project-page">
<Sidebar />
<div class="page-content">
{page}
</div>
{this.showIssueDetail && (
<Dialog open={true} onOpenChange={...}>
<IssueDetails issueId={this.issueId} />
</Dialog>
)}
</div>
)
}
}The route config determines which child page resolves to:
'/project': {
layout: Project,
children: {
'/board': Board,
'/board/issues/:issueId': Board,
'/settings': ProjectSettings,
},
}{page} renders the resolved child — no manual router.path checks needed for view switching.issueId are available on router.params for overlay logic (dialogs, modals).React typically uses useState for errors plus ad-hoc validation or a library like Formik/Yup.
Gea uses member variables for form state and a lightweight validator utility:
// utils/validation.ts
type Validator = (value: any) => string | false
export const is = {
required: (): Validator => value =>
(value === undefined || value === null || value === '') && 'This field is required',
maxLength: (max: number): Validator => value =>
!!value && value.length > max && `Must be at most ${max} characters`,
url: (): Validator => value =>
!!value && !/^https?:\/\//.test(value) && 'Must be a valid URL',
}
export function generateErrors(
fieldValues: Record<string, any>,
fieldValidators: Record<string, Validator | Validator[]>,
): Record<string, string> {
const errors: Record<string, string> = {}
for (const [name, validators] of Object.entries(fieldValidators)) {
const list = Array.isArray(validators) ? validators : [validators]
for (const validator of list) {
const msg = validator(fieldValues[name])
if (msg && !errors[name]) errors[name] = msg
}
}
return errors
}// Component usage
export default class IssueCreate extends Component {
title = ''
errors: Record<string, string> = {}
async handleSubmit() {
this.errors = generateErrors(
{ title: this.title },
{ title: [is.required(), is.maxLength(200)] },
)
if (Object.keys(this.errors).length > 0) return
// proceed with API call
}
template() {
return (
<div>
<input
class={`input ${this.errors.title ? 'input-error' : ''}`}
value={this.title}
input={(e: any) => { this.title = e.target.value }}
/>
{this.errors.title && <div class="form-error">{this.errors.title}</div>}
</div>
)
}
}@geajs/ui Select in FormsReact uses custom select components or react-select. Gea uses @geajs/ui Select:
import { Select } from '@geajs/ui'
// Single select — value is always an array, wrap/unwrap manually
<Select
class="w-full"
items={typeOptions}
value={[this.type]}
onValueChange={(d: { value: string[] }) => {
const v = d.value[0]
if (v !== undefined) this.type = v
}}
placeholder="Type"
/>
// Multi-select
<Select
class="w-full"
multiple={true}
items={userOptions}
value={this.userIds}
onValueChange={(d: { value: string[] }) => {
this.userIds = d.value
}}
placeholder="Assignees"
/>Key differences:
items is an array of { value, label } objects.value is always an array — for single select, wrap in [val] and extract d.value[0].onValueChange receives { value: string[] }, not the raw value.class="w-full" for full-width selects.React uses react-toastify or custom toast components. Gea uses @geajs/ui:
// stores/toast-store.ts
import { ToastStore } from '@geajs/ui'
const toastStore = {
success(title: string) {
ToastStore.success({ title })
},
error(err: unknown) {
ToastStore.error({
title: 'Error',
description: typeof err === 'string' ? err : (err as Error)?.message || String(err),
})
},
}
export default toastStoreAdd <Toaster /> to the App template:
import { Toaster } from '@geajs/ui'
template() {
return (
<div class="app">
<Project />
<Toaster />
</div>
)
}React apps commonly use react-beautiful-dnd. Gea implements DnD with native events:
Draggable card:
export default class IssueCard extends Component {
_didDrag = false
handleClick() {
if (this._didDrag) return
router.push(`/project/board/issues/${this.props.issueId}`)
}
onDragStart(e: DragEvent) {
this._didDrag = true
e.dataTransfer?.setData('text/plain', this.props.issueId)
if (e.dataTransfer) e.dataTransfer.effectAllowed = 'move'
;(e.currentTarget as HTMLElement).classList.add('dragging')
}
onDragEnd(e: DragEvent) {
;(e.currentTarget as HTMLElement).classList.remove('dragging')
queueMicrotask(() => { this._didDrag = false })
}
template({ issueId, title }: any) {
return (
<div
class="issue-card"
draggable={true}
dragstart={(e: DragEvent) => this.onDragStart(e)}
dragend={(e: DragEvent) => this.onDragEnd(e)}
click={() => this.handleClick()}
>
{title}
</div>
)
}
}Drop target column:
<div
class="board-list-issues"
dragover={(e: DragEvent) => {
e.preventDefault()
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move'
;(e.currentTarget as HTMLElement).classList.add('board-list--drag-over')
}}
dragleave={(e: DragEvent) => {
const el = e.currentTarget as HTMLElement
const related = e.relatedTarget as Node | null
if (!related || !el.contains(related)) el.classList.remove('board-list--drag-over')
}}
drop={(e: DragEvent) => {
e.preventDefault()
;(e.currentTarget as HTMLElement).classList.remove('board-list--drag-over')
const id = e.dataTransfer?.getData('text/plain')
if (id) projectStore.moveIssueToColumn(id, status)
}}
>
{issues.map(issue => <IssueCard key={issue.id} ... />)}
</div>Key patterns:
_didDrag flag: Prevents click from firing after a drag. Reset via queueMicrotask to let the click event fire and be ignored first.dragleave child bubbling: Check e.relatedTarget to avoid false drag-leave when moving over child elements.dragging and board-list--drag-over directly on the element.React uses useEffect with a debounce timeout or a library. Gea uses a member variable timer:
export default class IssueSearch extends Component {
searchTerm = ''
matchingIssues: any[] = []
isLoading = false
_debounceTimer: any = null
handleInput(e: any) {
this.searchTerm = e.target.value
clearTimeout(this._debounceTimer)
if (this.searchTerm.trim()) {
this._debounceTimer = setTimeout(() => this.doSearch(), 300)
} else {
this.matchingIssues = []
}
}
async doSearch() {
this.isLoading = true
try {
const data = await api.get('/issues', { searchTerm: this.searchTerm.trim() })
this.matchingIssues = data || []
} catch {
this.matchingIssues = []
} finally {
this.isLoading = false
}
}
template({ onClose }) {
return (
<div class="issue-search">
<input
type="text"
autofocus
placeholder="Search issues..."
value={this.searchTerm}
input={(e: any) => this.handleInput(e)}
/>
{this.isLoading && <Spinner size={20} />}
{this.matchingIssues.map(issue => (
<Link key={issue.id} to={`/project/board/issues/${issue.id}`} onNavigate={() => onClose?.()}>
{issue.title}
</Link>
))}
</div>
)
}
}Key patterns:
setTimeout / clearTimeout: Store the timer as a member variable, clear on each keystroke.Link with onNavigate: Close the search modal when a result is clicked using the onNavigate callback.matchingIssues to [] when the search term is cleared.React uses useEffect to attach keydown listeners. Gea uses created() + dispose():
export default class CommentCreate extends Component {
isFormOpen = false
private _onKey: ((e: KeyboardEvent) => void) | null = null
created() {
this._onKey = (e: KeyboardEvent) => {
const tag = (e.target as HTMLElement).tagName
if (tag === 'INPUT' || tag === 'TEXTAREA' || (e.target as HTMLElement).isContentEditable) return
if (e.key === 'm' || e.key === 'M') {
e.preventDefault()
this.openForm()
}
}
document.addEventListener('keydown', this._onKey)
}
dispose() {
if (this._onKey) document.removeEventListener('keydown', this._onKey)
super.dispose()
}
}Always call super.dispose() when overriding dispose() in a class component.
Update local state immediately, send API request, revert on failure:
async updateIssue(fields: any): Promise<void> {
if (!this.issue) return
const currentFields = { ...this.issue }
Object.assign(this.issue, fields)
projectStore.updateLocalProjectIssues(this.issue.id, fields)
try {
await api.put(`/issues/${this.issue.id}`, fields)
} catch {
Object.assign(this.issue, currentFields)
projectStore.updateLocalProjectIssues(this.issue.id, currentFields)
}
}React uses useState for editing state. Gea uses member variables:
export default class IssueDetails extends Component {
isEditingTitle = false
editTitle = ''
startEditTitle() {
this.editTitle = issueStore.issue?.title || ''
this.isEditingTitle = true
}
saveTitle() {
this.isEditingTitle = false
if (this.editTitle.trim() && this.editTitle !== issueStore.issue?.title) {
issueStore.updateIssue({ title: this.editTitle.trim() })
}
}
template() {
return (
<div>
{!this.isEditingTitle && (
<h2 click={() => this.startEditTitle()}>{issueStore.issue?.title}</h2>
)}
{this.isEditingTitle && (
<textarea
value={this.editTitle}
input={(e: any) => { this.editTitle = e.target.value }}
blur={() => this.saveTitle()}
keydown={(e: any) => {
if (e.key === 'Enter') { e.preventDefault(); this.saveTitle() }
}}
></textarea>
)}
</div>
)
}
}Instead of useOnOutsideClick with mousedown listeners, use an overlay <div>:
export default class IssueDetails extends Component {
openDropdown: string | null = null
toggleDropdown(name: string) {
this.openDropdown = this.openDropdown === name ? null : name
}
closeDropdown() {
this.openDropdown = null
}
template() {
return (
<div class="issue-details-right">
{this.openDropdown && <div class="dropdown-overlay" click={() => this.closeDropdown()}></div>}
<div class="field field--relative">
<button click={() => this.toggleDropdown('status')}>Status</button>
{this.openDropdown === 'status' && (
<div class="custom-dropdown">
{statusOptions.map(opt => (
<div key={opt.value} click={() => {
issueStore.updateIssue({ status: opt.value })
this.closeDropdown()
}}>
{opt.label}
</div>
))}
</div>
)}
</div>
</div>
)
}
}This avoids global event listeners. The overlay is a transparent full-screen <div> positioned behind the dropdown that catches clicks.
When a component reads from a store that holds per-view data (e.g. a single issue), clear the store when leaving:
closeIssueDetail() {
issueStore.clear()
router.push('/project/board')
}// In the store
clear(): void {
this.issue = null
this.isLoading = false
}@geajs/ui ComponentsMany React apps have custom implementations of common UI elements. Replace them with @geajs/ui:
| Custom React Component | @geajs/ui Replacement | Key Props |
|---|---|---|
<Modal> | <Dialog> | open, onOpenChange, title, description |
<Button> | <Button> | variant ("default", "destructive", "ghost"), disabled, click |
<Select> | <Select> | items, value (array), onValueChange, multiple, placeholder |
<Avatar> | <Avatar> | src, name, class |
<Toast> / notifications | <Toaster> + ToastStore | Place <Toaster /> in App root |
<Link> | <Link> (from @geajs/core) | to, class, onNavigate |
import { Dialog, Button } from '@geajs/ui'
export default class ConfirmModal extends Component {
isOpen = false
open() { this.isOpen = true }
close() { this.isOpen = false }
handleConfirm() {
this.props.onConfirm?.()
this.close()
}
template({ title = 'Confirm', message = 'Are you sure?', confirmText = 'Confirm' }) {
return (
<Dialog
open={this.isOpen}
onOpenChange={(d: { open: boolean }) => { if (!d.open) this.close() }}
title={title}
description={message}
>
<div class="flex gap-2 justify-end mt-4">
<Button variant="default" click={() => this.handleConfirm()}>{confirmText}</Button>
<Button variant="ghost" click={() => this.close()}>Cancel</Button>
</div>
</Dialog>
)
}
}Use this checklist when converting a React app to Gea:
package.json, vite.config.ts, index.html, main.ts@geajs/core, @geajs/vite-plugin, @geajs/ui:rootlocalStorage helpers (usually copy as-is)is.required(), is.maxLength(), generateErrors()@geajs/ui ToastStoreRouter in router.ts, setRoutes with guards/layouts/redirects in App.tsxcreated() for async auth + data fetching, <Outlet /> + <Toaster /> in templatepage prop from router@geajs/ui Dialog with open + onOpenChange@geajs/ui Select for dropdowns, @geajs/ui Button for actions@geajs/ui Avatar; custom Icon, Spinner as function componentsonClick → click, onChange → input/changeclass attributes + inline style for computed values_didDrag flag for click suppressioncreated() + dispose() with super.dispose()setTimeout / clearTimeout in member variablesclear() methods for per-view stores when navigating away© dashersw, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
SKILL.md and 1 other file in .cursor/skills/react-to-gea-migration of dashersw/gea.
Open the folder on GitHubat commit 51da237
React To Gea Migration next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| React To Gea Migration this skilldashersw/gea | 1.3k | — | ~8.1k | Automated safety check: Pass | MIT | |
| Dify Component Writing Guidelanggenius/dify | 158k | — | ~626 | Automated safety check: Pass | Custom licence | |
| Pierre Theming Librarypierrecomputer/pierre | 6.2k | — | ~661 | Automated safety check: Pass | Apache-2.0 | |
| Frontend Patternskurealnum/dotfiles | 290 | 19 repos | ~3.7k | Automated safety check: Pass | None | |
| Cursor BYOK Frontend Guideleookun/cursor-byok | 3.2k | — | ~1.9k | Automated safety check: Pass | MIT | |
| Wireframeakonan/wiremd | 103 | — | ~2.8k | Automated safety check: Pass | MIT |
langgenius/dify
Use when implementing or refactoring React/TypeScript components and the task requires decisions about component ownership, feature boundaries, state, data…
pierrecomputer/pierre
Explains how to use the @pierre/theming package to list, resolve, select, switch and persist themes, with controller, color and React references.
kurealnum/dotfiles
Frontend development patterns for React, Next.js, state management, performance optimization, and UI best practices.
leookun/cursor-byok
Sets the rules for building the Cursor BYOK desktop app's React and Tauri frontend, especially its HTTP boundary and component state architecture.
akonan/wiremd
Create and render wireframes using wiremd — a text-first tool that converts Markdown into visual HTML mockups.
gabeklein/expressive-mvc
Class-based reactive state management for React (Expressive MVC).
dashersw/gea
A skill your agent uses when creating Gea applications, components, stores, routes, SSR entry points, JSX templates, or working with Gea reactivity and DOM patching.
dashersw/gea
Guide for using @geajs/ui — a Tailwind-styled, Zag.js-powered component library for the Gea framework.
dashersw/gea
Rebuild the website playground browser bundles after changing @geajs/core or @geajs/vite-plugin source.
Categories
Step-by-step guide for migrating React applications to Gea — covering project setup, component conversion, state management, routing, styling, and known pitfalls. React To Gea Migration is an agent skill from dashersw/gea. Step-by-step guide for migrating React applications to Gea — covering project setup, component conversion, state management, routing, styling, and known pitfalls.
React To Gea Migration fits situations like: converting an existing React codebase to Gea; advising on migration strategy.
Run `npx skills add dashersw/gea --skill react-to-gea-migration -a claude-code`. Or copy the skill folder (.cursor/skills/react-to-gea-migration in dashersw/gea) into .claude/skills/react-to-gea-migration in your project. Claude Code loads it when a task matches its description.
Run `npx skills add dashersw/gea --skill react-to-gea-migration -a codex`. Or copy the skill folder (.cursor/skills/react-to-gea-migration in dashersw/gea) into .agents/skills/react-to-gea-migration in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add dashersw/gea --skill react-to-gea-migration -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/react-to-gea-migration, .gemini/skills/react-to-gea-migration, .github/skills/react-to-gea-migration and .opencode/skills/react-to-gea-migration in your project.
SKILL.md names no scripts, command-line tools or credentials: React To Gea Migration is instructions for the agent only.
SKILL.md names 1 domain. As links in the text: github.com. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
React To Gea Migration is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 8.1k tokens (SKILL.md is roughly 33k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with React To Gea Migration: Dify Component Writing Guide (langgenius/dify, 158k stars), Pierre Theming Library (pierrecomputer/pierre, 6.2k stars), Frontend Patterns (kurealnum/dotfiles, 290 stars) and Cursor BYOK Frontend Guide (leookun/cursor-byok, 3.2k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
dashersw (a GitHub user) maintains it in dashersw/gea, which has 1,257 GitHub stars. The repository holds 4 skills in this directory. The repository was last updated on September 30, 2026.
Source: dashersw/gea on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.