Back to Blog

WeWeb to React: Migration guide

·

WeWeb To React Guide

Thesis

Migrating a WeWeb app to React is not a frontend syntax rewrite.

A WeWeb export is a runtime that interprets a serialized application definition: routes, page JSON, collections, variables, workflows, plugins, sections, and generated elements. If you treat that export like a normal Vue codebase and translate it file-by-file into JSX, you will almost certainly carry over the weakest parts of the original system:

  • generic state
  • hidden business logic
  • weak module boundaries
  • UI coupled to tool-generated structure
  • runtime-driven behavior that should become explicit application code

The right way to migrate is to use the WeWeb export as a specification of product behavior, then rebuild that behavior in React with a proper technical architecture.

This guide uses the sprout-weweb example as a concrete reference, but the approach is designed to work for most WeWeb migrations.

What A WeWeb Export Actually Is

In sprout-weweb, the important insight is that the app is not authored mainly as route components with explicit domain code. It is assembled at runtime.

The clearest evidence:

  • src/_front/router.js builds routes from window.wwg_designInfo, including page metadata, language handling, auth gating, and dynamic imports.
  • src/wwLib/services/wwWebsiteData.js fetches page JSON and injects pages, sections, collections, variables, formulas, workflows, and library components into runtime state.
  • src/pinia/variables.js acts as a generic variable system for website variables, plugin variables, component variables, query params, and local storage persistence.
  • src/_common/helpers/code/workflows.js is a generic workflow engine that executes actions like navigation, variable updates, collection fetches, conditional branches, popups, and plugin calls.
  • src/wwLib/services/wwCollection.js abstracts data fetching behind collection configuration and plugin adapters rather than a normal typed API layer.

So the export is closer to this:

flowchart LR
    wewebConfig[WeWebConfig]
    routerRuntime[RouteRuntime]
    pageData[PageJSON]
    variableRuntime[VariableRuntime]
    workflowRuntime[WorkflowRuntime]
    pluginAdapters[PluginAdapters]
    renderedApp[RenderedApp]

    wewebConfig --> routerRuntime
    wewebConfig --> pageData
    wewebConfig --> pluginAdapters
    pageData --> variableRuntime
    pageData --> workflowRuntime
    pluginAdapters --> workflowRuntime
    routerRuntime --> renderedApp
    variableRuntime --> renderedApp
    workflowRuntime --> renderedApp

That is why the migration should begin with interpretation, not translation.

The Goal Of The Migration

The goal is not:

  • convert Vue files to React files
  • keep the same runtime model in a different framework
  • preserve every WeWeb abstraction

The goal is:

  • extract the app's product behavior
  • classify behavior by architectural concern
  • rebuild those concerns with explicit ownership
  • end up with a codebase that is easier to extend than the original app

If the new React app still behaves like a no-code runtime under the hood, the migration failed even if the UI looks correct.

The Sprout Example: What To Learn From It

The Sprout example is a good migration reference because it clearly shows the difference between application behavior and WeWeb implementation mechanics.

FUNCTIONALITY-ANALYSIS.md shows a 30-page product across several domains:

  • authentication
  • family portal
  • admin operations
  • reporting
  • commerce
  • calendar and scheduling
  • organization and location switching

That domain inventory is far more valuable for the migration than the generated component tree.

The most important recurring behaviors in the example are:

  • route-level private/public access
  • role-based redirects for admin pages
  • query-param-based page initialization
  • app-level initialization workflows
  • collection-driven data loading
  • popup-driven detail flows
  • calendar and date initialization
  • shopping cart and transaction behavior
  • organization/location context switching

Those are the things you must preserve.

The exact WeWeb-generated layout and element structure is mostly not worth preserving.

The First Reframe: Separate Requirements From Tool Artifacts

Every WeWeb concept should be categorized into one of two buckets.

Product Requirements

These must survive the migration:

  • business entities
  • user roles and access rules
  • routes and user flows
  • data dependencies
  • form submissions and validation rules
  • user-visible states
  • side effects like redirects, notifications, uploads, or policy signatures

WeWeb Implementation Artifacts

These usually should not survive as-is:

  • generic workflow graphs
  • generic variable registry
  • collection IDs as a frontend abstraction
  • generated sections as long-term component boundaries
  • page JSON as a permanent UI composition model
  • plugin-specific runtime conventions when a simpler service boundary is possible

This distinction is the foundation of the architecture.

How To Read A WeWeb App Before Rebuilding It

Use the export as a behavioral map.

1. Start With The Page Inventory

In Sprout, FUNCTIONALITY-ANALYSIS.md is already a good page index. If you do not have this document, derive it from page data.

For every page, capture:

  • route
  • access model
  • purpose
  • primary user actions
  • key forms
  • workflows triggered on load or submit
  • upstream data dependencies
  • downstream navigation effects

This gives you the product surface area.

2. Identify Repeated App-Level Patterns

In Sprout, repeated patterns matter more than one-off page details.

Examples:

  • admin permission enforcement across several pages
  • query-driven initialization for reports and reset-password flows
  • redirect pages used for organization and location changes
  • shared navigation/sidebar structure
  • popups used to display details without leaving the page

These repeated patterns will become reusable React architecture, not one-off patches.

3. Trace The Runtime Entry Points

In Sprout, the runtime entry points are more important than individual generated components.

Use these files first:

  • src/_front/router.js
  • src/wwLib/services/wwWebsiteData.js
  • src/_common/helpers/data/index.js
  • src/_common/helpers/code/workflows.js
  • src/wwLib/services/wwCollection.js
  • src/pinia/variables.js

Together, these explain:

  • when page state resets
  • when auth is checked
  • when variables are initialized
  • when workflows run
  • when collections fetch
  • when navigation happens

That tells you where the real behavior is hiding.

4. Convert Runtime Concepts Into A Domain Map

Before writing any React code, rewrite the system in domain language.

For example:

  • "collection 476bb250-..." should become "current organizations data" or "family transaction list"
  • "workflow 14ae93a6-..." should become "admin access guard"
  • "query variable" should become "URL-owned filter state"
  • "open popup action" should become "detail modal flow"

If the React migration keeps speaking in WeWeb IDs and generic action types, the architecture is still too close to the old system.

How To Translate WeWeb Concepts Into React Concepts

Here is the mental mapping that produces good architecture.

WeWeb concept What it means React target
Page User-facing route or screen Route module plus feature module
Section Reusable layout/content fragment Layout component or domain component
Element UI primitive or widget Design-system primitive or feature component
Workflow Implicit event/state graph Explicit route logic, service logic, mutation handler, or UI event handler
Collection Data source or query Typed API function plus server-state query
Variable Mixed state container URL state, form state, local UI state, persisted preference, or session state
Plugin Integration/runtime adapter Service client, SDK wrapper, or infrastructure module
Formula Derived logic or expression TypeScript utility, selector, formatter, or domain rule

This mapping matters because it prevents you from re-implementing WeWeb instead of building an app.

The Target React Architecture

You need explicit ownership boundaries. A good default architecture for a serious migration looks like this.

src/
  app/
    router/
    providers/
    layouts/
    guards/
  features/
    auth/
    families/
    students/
    activities/
    reports/
    commerce/
    settings/
  entities/
    user/
    organization/
    class/
    transaction/
  shared/
    ui/
    forms/
    api/
    lib/
    config/
    types/
  pages/
    login/
    dashboard/
    admin-families/
    family-profile/
    reports-financial/

You can vary the exact folder names, but the ownership model should stay stable.

App Layer

Owns:

  • routing
  • providers
  • layout shell
  • auth boundary
  • global error handling
  • app bootstrap

This is where private route enforcement and cross-cutting providers live.

Feature Layer

Owns:

  • domain workflows
  • route loaders/actions
  • query hooks
  • mutations
  • screens for a bounded area

For Sprout, this means separate feature modules for auth, reports, activities, commerce, and family management.

Shared Layer

Owns:

  • API clients
  • common UI
  • form abstractions
  • schema utilities
  • formatting and generic helpers

Shared should not become a dumping ground for business logic.

Entity Layer

Owns:

  • core business types
  • reusable selectors
  • small domain utilities

Use it when several features depend on the same core object model.

Recommended Technical Defaults

For most migrations, these are strong defaults:

  • React Router for route modules and data-aware routing
  • TanStack Query for server state and mutation lifecycle
  • React Hook Form plus Zod for form state and validation
  • a typed API client layer for Xano or the replacement backend
  • a minimal client-state solution only for truly local or app-shell state
  • a proper component library or internal design system for UI primitives

The point is not the exact library choice. The point is separation of concerns.

What Not To Recreate In React

Avoid rebuilding these WeWeb patterns in React:

1. A Giant Generic Variable Store

src/pinia/variables.js mixes:

  • query parameters
  • local storage
  • website variables
  • plugin variables
  • component variables
  • mutable objects and arrays

In React, these should be split by ownership:

  • URL state belongs in the router or search params
  • server state belongs in query/mutation caching
  • form state belongs in the form library
  • local UI state belongs in components
  • persistent preferences belong in a small persistence abstraction
  • session/auth state belongs in an auth module

If you create a single global store to mirror WeWeb variables, you will preserve the worst part of the old architecture.

2. A Workflow Interpreter

src/_common/helpers/code/workflows.js is useful for understanding behavior, but it is not a good target architecture.

That file currently handles:

  • control flow
  • navigation
  • data fetching
  • state mutation
  • uploads
  • popups
  • plugin calls
  • language changes
  • logging

In React, these responsibilities should be pushed into explicit modules with names that reflect business intent.

Instead of:

  • "execute workflow 14ae93a6-..."

You want:

  • requireAdminAccess()
  • initializeFinancialReportFiltersFromUrl()
  • switchOrganizationAndRedirectHome()
  • submitClassCreationForm()

3. A Collection Abstraction As Your Main Data Model

src/wwLib/services/wwCollection.js is a runtime-oriented abstraction that fetches through plugin adapters and configuration bindings.

That is acceptable inside a no-code runtime. It is not how you should design a long-term React frontend.

Your React app should prefer:

  • typed request functions
  • route loaders where appropriate
  • query hooks with stable keys
  • mutations with clear invalidation rules
  • domain names instead of collection IDs

Rebuilding Data The Right Way

A WeWeb app often blurs the line between server state and client state. Your React app should make that distinction explicit.

Server State

This includes:

  • current user
  • organizations and locations
  • families
  • activities
  • reports
  • transactions
  • class details
  • policy content

Server state should be fetched, cached, invalidated, and refetched by server-state tooling.

In Sprout, collection fetching currently happens during runtime initialization and workflow execution. In React, that should become route loaders or query hooks close to the consuming feature.

Client State

This includes:

  • open modal
  • selected tab
  • current calendar view mode
  • optimistic draft state
  • temporary filters not worth URL persistence

Keep this local by default.

URL State

Sprout uses query-driven initialization in several places:

  • reset password
  • report date ranges
  • view mode
  • duplication flows
  • embed filters

That is a strong signal that these values belong to the URL in the new app too.

Do not hide them in a store.

Persistent Preference State

If a value must persist between navigations or sessions, isolate that behavior in a small module:

  • theme
  • selected organization
  • selected location
  • last-used view mode

Do not mix this with business entities.

Rebuilding Routing And Navigation

src/_front/router.js is one of the most useful files in the Sprout export because it shows how the app currently thinks about routing.

It reveals:

  • routes are generated from window.wwg_designInfo
  • auth checks happen at route-entry time
  • page modules are dynamically imported
  • page data is fetched before render
  • language and hash behavior are part of routing

In React, make routing explicit.

Recommended Routing Model

Build route modules for stable business routes:

  • /login
  • /signup
  • /home
  • /admin/families
  • /admin/families/:familyOrganizationId
  • /activity-calendar
  • /financial-reports
  • /class-detail/:classId

Each route module should own:

  • route-level access requirements
  • route params parsing
  • loader logic when required before render
  • route-local error boundaries when needed

Auth And Permission Checks

In Sprout, auth and permission logic is split between route metadata, auth plugin state, and workflows.

In React, consolidate this into one access system:

  • auth provider or session boundary
  • permission model
  • route guard or loader enforcement

A good rule:

  • authentication decides whether the user may enter the private area
  • authorization decides which feature areas the user may access
  • feature code should not repeatedly reinvent redirect logic

Redirect Pages

Sprout uses special redirect pages for organization and location changes.

In React, those flows can become explicit mutation or command handlers:

  • update active organization
  • invalidate dependent queries
  • navigate to the intended landing page

You may still keep a dedicated transition route if it improves UX, but it should be a conscious product decision, not a byproduct of the old runtime.

Rebuilding Auth Properly

In the Sprout example, auth is spread across:

  • route checks in router.js
  • Xano auth plugin state
  • app-level workflows like logout on invalid account
  • organization and location initialization

That spread is normal in a no-code runtime. It is not ideal in a codebase.

Better React Auth Boundary

Create one auth module that owns:

  • session retrieval
  • token storage strategy
  • current user bootstrap
  • invalid session handling
  • unauthorized redirects
  • role and permission evaluation

It should expose explicit concepts:

  • isAuthenticated
  • currentUser
  • permissions
  • activeOrganization
  • activeLocation

Do not force feature modules to know how tokens or plugin state work.

Handle Multi-Context Auth Cleanly

Sprout clearly has organization and location context. That means auth is not just "logged in or not."

You likely need:

  • session context
  • organization context
  • location context
  • permission matrix derived from user plus context

This should be modeled explicitly in TypeScript types and selectors.

Rebuilding Forms Instead Of Translating Them

WeWeb forms often hide logic inside variables, workflows, and component state. That does not mean your React version should.

Every Important Form Should Be Explicit

For each form, define:

  • schema
  • default value source
  • submission contract
  • validation rules
  • success path
  • error handling
  • optimistic or loading behavior

Examples from Sprout:

  • login
  • signup
  • reset password
  • family profile editing
  • class creation
  • report filters
  • cart and checkout actions

Recommended Form Pattern

For each form:

  1. derive defaults from route loader, query params, or fetched server state
  2. validate with a schema
  3. submit through a typed mutation
  4. update cache intentionally
  5. navigate or notify based on a defined success policy

This is much clearer than a workflow graph that mutates generic variables and then changes page conditionally.

Rebuilding UI Composition And The Design System

Generated sections are helpful for understanding layout patterns, but they are rarely the right long-term component boundaries.

In Sprout, recurring sections include:

  • sidebar navigation
  • loaders
  • branding
  • headers
  • modal containers

Those should become intentional design-system or shell-level components.

A Better UI Hierarchy

Use three levels:

1. Primitive UI Components

Examples:

  • buttons
  • inputs
  • selects
  • modals
  • tables
  • badges

2. App Shell Components

Examples:

  • sidebar
  • top navigation
  • page container
  • authenticated layout
  • section header

3. Feature Components

Examples:

  • family list panel
  • student profile card
  • report date range toolbar
  • transaction ledger table
  • class schedule panel

This hierarchy gives you components that reflect the product, not the no-code builder.

How To Translate Workflows Into Explicit Logic

This is the most important migration skill.

A workflow is not a target implementation. It is evidence of behavior.

For each workflow, ask:

  • is this route-entry logic?
  • is this domain orchestration?
  • is this UI event handling?
  • is this just derived state?
  • is this a side effect triggered by a mutation?

Then move it to the right place.

Workflow Translation Patterns

Pattern 1: Auth Redirects

Current WeWeb form:

  • route metadata plus workflow branch plus change-page action

React form:

  • route guard or loader
  • central auth redirect helper
  • permission-aware layout boundary

Pattern 2: Query-Based Initialization

Current WeWeb form:

  • query variable plus before-collection-fetch workflow

React form:

  • parse search params in route or feature hook
  • normalize into typed filter state
  • pass into query keys or form defaults

Pattern 3: Popup Detail Flow

Current WeWeb form:

  • fetch collection, then open popup

React form:

  • selected entity ID in URL or local state
  • query for detail data
  • modal component with explicit props and loading states

Pattern 4: Redirect Pages

Current WeWeb form:

  • enter page, run workflow, navigate away

React form:

  • explicit command handler or transition route with a narrow purpose

Pattern 5: Calendar Initialization

Current WeWeb form:

  • on-load workflow sets date variables and view state

React form:

  • route loader for required data
  • derived initial state from URL or defaults
  • feature hook that coordinates date range and filters

A Decision Framework For Every Piece Of Logic

When you discover a workflow or variable, classify it using this matrix.

If the logic does this Put it here
controls page access route guard or route loader
loads server data query hook or route loader
updates server data mutation handler or feature service
controls URL filters search params parser plus URL sync
drives only one component local component state
is shared domain behavior feature service or domain utility
opens or closes overlays local UI state or shell-level modal manager
transforms fetched data for display selector or pure utility

This is how you turn hidden runtime behavior into architecture.

The Migration Should Be Feature-Sliced, Not Syntax-Sliced

Do not migrate page JSON, then sections, then elements, then workflows, one layer at a time. That produces a React app that mirrors the no-code runtime.

Instead, migrate by coherent business slices.

A Good Order For A Real Project

1. App Shell And Auth

Build:

  • router
  • auth boundary
  • layouts
  • session bootstrap
  • permission checks

This becomes the foundation for every other slice.

2. One Representative Private Feature

Pick a route area with real depth, such as:

  • family profile
  • class detail
  • activity calendar

This forces you to prove your patterns for routing, data, forms, and UI.

3. One Admin Slice

Pick something like:

  • admin families
  • financial reports
  • cancellation reviews

This validates permission boundaries and more complex workflows.

4. Shared Cross-Cutting Systems

Once patterns are proven, extract:

  • table patterns
  • modal patterns
  • filter bars
  • form building blocks
  • query key conventions

5. Commerce And Edge Flows

Then handle:

  • cart
  • transactions
  • checkout-like interactions
  • embed flows
  • organization/location switching

That order reduces architectural rework.

How To Handle Xano And External Integrations

Sprout uses Xano through WeWeb plugins. In the migration, Xano should become an explicit backend integration boundary.

Prefer:

  • typed API clients
  • request/response schemas
  • centralized auth header handling
  • narrow service modules per feature

Avoid:

  • plugin-shaped abstractions leaking into feature code
  • generic "run backend action" helpers everywhere

If you later replace Xano, your React app should not need a full rewrite of feature modules.

Anti-Patterns That Will Hurt The Migration

Anti-Pattern 1: Keeping WeWeb IDs In Application Logic

If your new code still talks in opaque collection IDs and workflow IDs, your architecture is not clean enough.

Anti-Pattern 2: Building A React No-Code Runtime

If you end up with:

  • generic action registries
  • giant config-driven flow execution
  • one universal state registry
  • dynamic rendering for everything

you have rebuilt the old problem in a different framework.

Anti-Pattern 3: Preserving Generated Component Boundaries

Generated sections are not automatically good component boundaries.

Use them as clues, not as the final structure.

Anti-Pattern 4: Mixing Entity Data With UI State

This usually happens when teams port variable-based logic too literally.

Keep domain data, route state, and UI state separate.

Anti-Pattern 5: Treating Migration As Pure QA Parity

Parity matters, but parity alone is not enough.

You also need:

  • maintainability parity
  • observability parity
  • type safety improvements
  • testability improvements

Otherwise the migration cost will not pay off.

A Practical Architecture For The Sprout-Type App

If I were rebuilding the Sprout example, I would target something like this:

flowchart TD
    browser[Browser]
    router[ReactRouter]
    authBoundary[AuthBoundary]
    layouts[AppLayouts]
    features[FeatureModules]
    queryLayer[ServerStateLayer]
    apiLayer[TypedAPILayer]
    xano[XanoBackend]

    browser --> router
    router --> authBoundary
    authBoundary --> layouts
    layouts --> features
    features --> queryLayer
    queryLayer --> apiLayer
    apiLayer --> xano

Supporting rules:

  • route loaders handle access and route-owned initialization
  • TanStack Query owns fetched entities and cache invalidation
  • feature modules own mutations and business workflows
  • forms own their own schemas and submission pipelines
  • shell state stays outside business entities
  • shared UI primitives are separated from feature widgets

Production-Ready Fundamentals The New App Should Have

This is the real standard for success.

Explicit Boundaries

Everyone should know where to put:

  • routing logic
  • auth logic
  • data fetching
  • form logic
  • feature orchestration
  • UI primitives

Typed Contracts

The new app should have typed contracts for:

  • API requests
  • API responses
  • route params
  • search params
  • domain entities
  • form schemas

Observable Behavior

You should be able to inspect:

  • failed queries
  • failed mutations
  • auth state transitions
  • navigation failures
  • important business events

Testable Domain Flows

Critical paths should be testable without emulating a workflow engine:

  • login
  • permission redirects
  • class creation
  • report filtering
  • cart updates
  • organization switching

Design System Discipline

The new app should not devolve into page-specific component sprawl.

A Good Migration Heuristic

For every piece of WeWeb logic, ask:

  1. what user-facing behavior does this create?
  2. what business rule is hiding behind it?
  3. who should own that rule in a normal React app?
  4. can I implement that rule without recreating the WeWeb runtime?

If the answer to the last question is no, you are still too close to the old architecture.

Final Principle

The purpose of this migration is not to prove that React is better than WeWeb.

The purpose is to move from a runtime-interpreted app to an explicit, maintainable software system.

Use the WeWeb export to understand:

  • what the product does
  • what data it depends on
  • what rules govern behavior
  • what flows matter to users

Then rebuild those things with clear architecture, explicit ownership, and code that a real engineering team can safely evolve.

That is what makes the migration worth doing.

Want to learn more?

Book a call to discuss how we can help your project

Book a free call