Skip to content

Uncontrolled-only Philosophy

Why Uncontrolled?

Extable adopts uncontrolled-only integration to keep data lifecycle explicit and predictable.

The Problem with Controlled Props

In a traditional controlled-component model, the parent component owns state and re-renders to sync the table. For a complex UI like a table:

  • State bloat: selection, pending edits, undo/redo history, view filters/sorts — all become parent concerns.
  • Re-render overhead: every keystroke, click, or scroll may trigger parent re-renders (expensive for large tables).
  • Implicit automation: React/Vue frameworks auto-sync props, making it unclear when data actually changes at the server level.

Extable's Approach: Explicit Responsibility

Extable inverts this: the core library owns its own UI state (selection, editing, view), and the developer owns the data lifecycle.

This keeps concerns separated:

ConcernOwnerResponsibility
UI state (selection, edit mode, view filters)Extable coreInternal, reactive, invisible to parent
Data lifecycle (fetch, cache, mutation, refresh)DeveloperExplicit, controllable, visible in code

Developer Responsibility: Two Steps

As an extable user, your responsibility is minimal and explicit:

  1. Initial load: pass defaultData to the component
  2. After mutation: call setData() with fresh data (after your API call)

That's it. No automatic syncing, no magic — just you deciding when to update.

Code Patterns

javascript
const core = new ExtableCore({
  root: document.getElementById('table'),
  defaultData: initialData,
  defaultView: defaultView,
  schema: schema,
});

// Listen to table state changes (e.g., when user edits)
core.subscribeTableState((nextState, prevState, reason) => {
  console.log('Table changed:', reason);
  
  // Example: when the table detects edits, fetch and display current data
  if (reason === 'edit' || reason === 'commandExecuted') {
    handleTableChanged();
  }
});

async function handleTableChanged() {
  try {
    // Fetch fresh data from your API
    const response = await fetch('/api/table');
    const freshData = await response.json();
    
    // Explicitly update the table with new data
    core.setData(freshData);
  } catch (error) {
    console.error('Failed to refresh:', error);
  }
}

// When user clicks Save button
document.getElementById('saveBtn').addEventListener('click', async () => {
  // Get the current table data (if needed for submission)
  // In this example, we just refresh and let the server handle persistence
  await handleTableChanged();
});

Benefits

  1. Clarity: No hidden re-fetches or caches. Your code controls when data updates.
  2. Flexibility: Integrate with any fetch library (SWR, React Query, Apollo, etc.) or none at all.
  3. Predictability: No magic props diffing. What you write is what happens.
  4. Testability: Data mutations are explicit function calls, easy to mock and test.

When to Update?

Update (call setData()) when:

  • User clicks a Save/Sync button
  • You receive a WebSocket push with fresh data
  • You poll the server and get new data
  • Multi-user sync completes

Don't call setData() automatically on:

  • Every keystroke (the table handles edits internally)
  • Navigation (unless you explicitly fetch new data)
  • Prop changes from the parent (pass it once via defaultData)

Next Steps

  • See GuidesCore quickstart for a minimal example.
  • See UsageEditing for details on how edits work internally.
  • See Reference for setData(), subscribeTableState(), and other imperative APIs.

Released under the Apache 2.0 License