Skip to content

Vue vs Gea

This is a technical comparison for developers evaluating Gea who already know Vue. Both frameworks use proxy-based reactivity, but they differ significantly in their template systems, component models, and rendering strategies.

Architecture

Vue

Vue uses a reactive virtual DOM. Templates (or JSX) are compiled into render functions that produce a virtual DOM tree. When reactive state changes, Vue re-executes the render function, diffs the new virtual tree against the old one, and patches the real DOM. Vue 3's compiler also performs static analysis to hoist static nodes and skip diffing them.

Gea

Gea uses compile-time patching with no virtual DOM. The Vite plugin analyzes your JSX at build time, determines exactly which DOM nodes depend on which state paths, and generates targeted patch functions. When state changes, only those patch functions run — there is no tree diffing, no render function re-execution, and no virtual DOM.

Practical impact: Vue's compiler optimizes the diffing step; Gea eliminates it entirely. For fine-grained updates (a single counter incrementing, a checkbox toggling), Gea touches only the affected DOM node. Vue achieves similar granularity through its compiler hints, but the underlying mechanism is fundamentally different.

Reactivity

Both frameworks use Proxy for reactivity, but the APIs differ significantly.

Vue

js
import { ref, reactive, computed, watch } from 'vue'

const count = ref(0)
const state = reactive({ todos: [], filter: 'all' })

const filteredTodos = computed(() => {
  if (state.filter === 'active') return state.todos.filter(t => !t.done)
  return state.todos
})

watch(() => state.filter, (newVal) => {
  console.log('Filter changed to', newVal)
})

Vue's reactivity API has multiple primitives:

  • ref() for single values (requires .value to access)
  • reactive() for objects
  • computed() for memoized derived values
  • watch() and watchEffect() for side effects
  • toRef(), toRefs(), shallowRef(), triggerRef(), etc.

Gea

ts
import { Store } from '@geajs/core'

class TodoStore extends Store {
  todos = []
  filter = 'all'

  get filteredTodos() {
    if (this.filter === 'active') return this.todos.filter(t => !t.done)
    return this.todos
  }
}

export default new TodoStore()

Gea's reactivity API is a single pattern:

  • Reactive properties per store, wrapped in a deep Proxy
  • Mutate directly — no .value, no reactive() wrapper
  • Getters for derived values (re-evaluate on access, not memoized)
  • observe() for side effects (usually generated by the Vite plugin)

Key difference: Vue requires choosing between ref and reactive, remembering .value, and managing the ref unwrapping rules. These are framework-invented concepts with their own rules. Gea's philosophy is that you shouldn't need any of that — just properties on a store that you mutate directly, like any normal JavaScript property assignment.

Templates vs JSX

Vue (Single-File Components)

vue
<template>
  <div class="counter">
    <span>{{ count }}</span>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const count = ref(0)
const increment = () => count.value++
const decrement = () => count.value--
</script>

Vue uses its own template syntax with:

  • for text interpolation
  • v-if, v-else, v-show for conditionals
  • v-for for lists
  • v-model for two-way binding
  • v-bind: (:) for attribute binding
  • v-on: (@) for event binding

Gea (JSX)

jsx
import { Component } from '@geajs/core'
import counterStore from './counter-store'

export default class Counter extends Component {
  template() {
    return (
      <div class="counter">
        <span>{counterStore.count}</span>
        <button click={counterStore.increment}>+</button>
        <button click={counterStore.decrement}>-</button>
      </div>
    )
  }
}

Gea uses standard JSX with:

  • {} for interpolation
  • {cond && <X />} for conditionals
  • .map() for lists
  • value + input for controlled inputs
  • Event attributes (click, input, change — also accepts onClick, onInput, onChange)

Comparison Table

FeatureVue templateGea JSX
Text interpolation{count}
Conditionalv-if="cond"{cond && <X />}
Listv-for="item in items"{items.map(item => ...)}
Two-way binding (inputs)v-model="text"value={text} + input={fn}
Two-way binding (props)v-model + defineEmits / defineModelDirect mutation on shared proxy
Event binding@click="handler"click={handler} or onClick={handler}
Dynamic class:class="{ active: isActive }"class={`${isActive ? 'active' : ''}`}
Attribute binding:disabled="flag"disabled={flag}

Component Model

Vue

Vue offers three component styles:

Options API:

js
export default {
  data() { return { count: 0 } },
  methods: { increment() { this.count++ } },
  computed: { doubled() { return this.count * 2 } }
}

Composition API:

js
export default {
  setup() {
    const count = ref(0)
    const increment = () => count.value++
    return { count, increment }
  }
}

<script setup> (recommended):

vue
<script setup>
const count = ref(0)
const increment = () => count.value++
</script>

Gea

Gea offers two component styles:

Class component:

jsx
export default class Counter extends Component {
  template() {
    return <div>{counterStore.count}</div>
  }
}

Function component:

jsx
export default function Display({ count }) {
  return <div>{count}</div>
}

Vue's flexibility means more choices to make (Options vs Composition, ref vs reactive, SFC vs JSX). Gea's philosophy is that JavaScript code should be simple and understandable — it offers two component styles that map directly to language constructs: class for stateful (OOP), function for stateless (functional). There are no framework-specific primitives to choose between.

Side-by-Side: Todo List

Vue

vue
<template>
  <div>
    <input v-model="draft" @keyup.enter="add" />
    <button @click="add">Add</button>
    <ul>
      <li v-for="todo in todos" :key="todo.id">
        <input type="checkbox" :checked="todo.done" @change="toggle(todo.id)" />
        <span>{{ todo.text }}</span>
        <button @click="remove(todo.id)">x</button>
      </li>
    </ul>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const todos = ref([])
const draft = ref('')

function add() {
  if (!draft.value.trim()) return
  todos.value.push({ id: Date.now(), text: draft.value, done: false })
  draft.value = ''
}

function toggle(id) {
  const todo = todos.value.find(t => t.id === id)
  if (todo) todo.done = !todo.done
}

function remove(id) {
  todos.value = todos.value.filter(t => t.id !== id)
}
</script>

Gea

ts
// todo-store.ts
import { Store } from '@geajs/core'

class TodoStore extends Store {
  todos = []
  draft = ''

  add() {
    if (!this.draft.trim()) return
    this.todos.push({
      id: Date.now(), text: this.draft, done: false
    })
    this.draft = ''
  }

  toggle(id) {
    const todo = this.todos.find(t => t.id === id)
    if (todo) todo.done = !todo.done
  }

  remove(id) {
    this.todos = this.todos.filter(t => t.id !== id)
  }
}

export default new TodoStore()
jsx
// app.tsx
import { Component } from '@geajs/core'
import store from './todo-store'

export default class TodoApp extends Component {
  template() {
    const { todos, draft } = store
    return (
      <div>
        <input
          value={draft}
          input={e => (store.draft = e.target.value)}
          keydown={e => { if (e.key === 'Enter') store.add() }}
        />
        <button click={store.add}>Add</button>
        <ul>
          {todos.map(todo => (
            <li key={todo.id}>
              <input type="checkbox" checked={todo.done} change={() => store.toggle(todo.id)} />
              <span>{todo.text}</span>
              <button click={() => store.remove(todo.id)}>x</button>
            </li>
          ))}
        </ul>
      </div>
    )
  }
}

Both use proxy-based reactivity with direct mutation. The main differences are:

  • Vue uses SFC template syntax; Gea uses JSX
  • Vue has v-model for two-way binding; Gea uses value + input
  • Vue keeps state in the component; Gea extracts it to a store

Props and Data Flow

Vue

Vue enforces a one-way data flow convention for props. Even though Vue uses proxies internally, it discourages direct mutation of props and provides framework-level abstractions for two-way binding:

vue
<!-- Parent.vue -->
<template>
  <Editor v-model:name="user.name" />
</template>

<script setup>
import { reactive } from 'vue'
const user = reactive({ name: 'Alice', age: 30 })
</script>
vue
<!-- Editor.vue -->
<template>
  <input :value="name" @input="$emit('update:name', $event.target.value)" />
</template>

<script setup>
defineProps(['name'])
defineEmits(['update:name'])
</script>

Vue's two-way binding requires framework-specific concepts: v-model, defineEmits, defineModel, update:propName events. Each is a framework invention with its own syntax and rules.

Gea

Gea's props follow JavaScript's native value semantics — no framework abstractions needed:

  • Primitives are one-way — the child gets a copy (JS pass-by-value).
  • Objects and arrays are two-way — the child gets the parent's reactive proxy (JS pass-by-reference). Mutating the object in the child updates the parent's DOM automatically.
jsx
// parent.tsx
export default class Parent extends Component {
  user = { name: 'Alice', age: 30 }

  template() {
    return (
      <div>
        <span>{this.user.name}</span>
        <Editor user={this.user} />
      </div>
    )
  }
}
jsx
// editor.tsx
export default class Editor extends Component {
  rename() {
    this.props.user.name = 'Bob'   // updates Parent's DOM too
  }

  template({ user }) {
    return (
      <div>
        <span>{user.name}</span>
        <button click={this.rename}>Rename</button>
      </div>
    )
  }
}

No emit, no v-model, no defineModel. The child mutates the shared proxy directly and every observer updates. This is how JavaScript already works — if you pass an object to a function, the function can mutate it and the caller sees the change.

Comparison Table

ConcernVueGea
Primitive propsOne-wayOne-way (JS pass-by-value)
Object/array propsOne-way by convention, emit for updatesTwo-way (same proxy reference)
Two-way binding syntaxv-model + defineEmits / defineModelDirect mutation — no special syntax
Child → parent communication$emit('update:prop', value)Direct mutation on shared proxy
Deep nestingProp drilling or provide/injectSame object reference at any depth

Bundle Size

StackMin+GzipIncludes
Vue 3.5~33 kbRendering only
Vue 3.5 + Vue Router 5 + Pinia 3~35 kbRendering + state + routing
Gea~13 kbRendering + state + routing

Vue includes a template compiler, virtual DOM runtime, reactivity system, and component runtime — but not a router or state manager. With Vue Router and Pinia, the total comes to ~35 kb gzipped. Gea moves most work to build time, ships state management and routing out of the box, and still comes in at ~13 kb — 2.7x smaller than a comparable Vue stack.

Directives vs Plain JavaScript

Vue's directive system (v-if, v-for, v-model, v-show, custom directives) is a domain-specific language embedded in HTML templates. It's powerful but adds concepts to learn — a vocabulary that exists only in Vue, not in JavaScript itself.

Gea's philosophy rejects inventing new syntax. Conditionals are && or ternary — standard JavaScript. Lists use .map() — a built-in array method. Two-way binding is value + input — explicit and obvious. There are no directives, no special syntax, and no custom DSL. Every expression in a Gea template is something a JavaScript developer already knows how to read.

Ecosystem

Vue has a rich ecosystem: Vue Router, Pinia, Nuxt, Vuetify, PrimeVue, VueUse, and a large community. The single-file component format has strong IDE support.

Gea provides the core framework, mobile UI primitives, a Vite plugin, a scaffolder, and a VS Code extension. It's a deliberately compact toolkit.

When to Choose Which

Choose Vue when:

  • You prefer template syntax over JSX
  • You need a mature ecosystem with routing, SSR (Nuxt), and component libraries
  • You want v-model and built-in two-way binding
  • You're building a large team project and want abundant resources and training materials

Choose Gea when:

  • You believe frameworks should not invent new programming concepts — just leverage the ones JavaScript already has
  • You want the smallest possible bundle size
  • You prefer JSX and want attribute names close to standard HTML
  • You value compile-time optimization that eliminates virtual DOM overhead entirely
  • You're building mobile web apps and want built-in view management and gesture support
  • You want a simpler reactivity API without ref/reactive/.value distinctions or framework-specific primitives like signals

Released under the MIT License.