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.jsbuilds routes fromwindow.wwg_designInfo, including page metadata, language handling, auth gating, and dynamic imports.src/wwLib/services/wwWebsiteData.jsfetches page JSON and injects pages, sections, collections, variables, formulas, workflows, and library components into runtime state.src/pinia/variables.jsacts as a generic variable system for website variables, plugin variables, component variables, query params, and local storage persistence.src/_common/helpers/code/workflows.jsis a generic workflow engine that executes actions like navigation, variable updates, collection fetches, conditional branches, popups, and plugin calls.src/wwLib/services/wwCollection.jsabstracts 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.jssrc/wwLib/services/wwWebsiteData.jssrc/_common/helpers/data/index.jssrc/_common/helpers/code/workflows.jssrc/wwLib/services/wwCollection.jssrc/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:
isAuthenticatedcurrentUserpermissionsactiveOrganizationactiveLocation
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:
- derive defaults from route loader, query params, or fetched server state
- validate with a schema
- submit through a typed mutation
- update cache intentionally
- 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-fetchworkflow
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:
- what user-facing behavior does this create?
- what business rule is hiding behind it?
- who should own that rule in a normal React app?
- 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.