Skip to main content

Data Flow

Understanding how data flows through the Vertical Template helps you build features that integrate correctly with the architecture.

State Management Overview

The template uses a unified Zustand store created dynamically from the tenant configuration:

Store Hooks Reference

useStoreValue

Read a value at a dot-notation path (reactive):

import { useStoreValue } from '@enterpriseaigroup/client';

function PropertyDisplay() {
const address = useStoreValue('property.selectedAddress');
const stage = useStoreValue('stage.current');

return (
<div>
<p>Address: {address}</p>
<p>Stage: {stage}</p>
</div>
);
}

useSetStore

Get the setter function for updating store values:

import { useSetStore } from '@enterpriseaigroup/client';

function StageButton() {
const setStore = useSetStore();

const goToPlanning = () => {
// Third argument is caller name for DevTools
setStore('stage.current', 'planning', 'StageButton');
};

return <button onClick={goToPlanning}>Next</button>;
}

useGlobalSelector

Create a custom selector for derived state:

import { useGlobalSelector } from '@enterpriseaigroup/client';

function UserGreeting() {
const displayName = useGlobalSelector(
(state) => state.user?.name ?? 'Guest'
);

return <p>Hello, {displayName}</p>;
}

useStoreGetter

Get a non-reactive getter (for callbacks, avoids stale closures):

import { useStoreGetter } from '@enterpriseaigroup/client';

function ChatSubmit() {
const getMessages = useStoreGetter('chat.messages');

const handleSubmit = useCallback(() => {
// Always returns current value, no stale closure
const messages = getMessages();
console.log('Messages:', messages.length);
}, [getMessages]);
}

useResetSlice

Reset a slice to its initial state:

import { useResetSlice } from '@enterpriseaigroup/client';

function ResetButton() {
const resetProperty = useResetSlice('property');

return (
<button onClick={resetProperty}>
Clear Property Selection
</button>
);
}

useClearPersistedStore

Clear all persisted state (useful for logout):

import { useClearPersistedStore } from '@enterpriseaigroup/client';

function LogoutButton() {
const clearStore = useClearPersistedStore();

const handleLogout = () => {
clearStore();
// Redirect to login
};
}

Store Bindings

Components receive props from the store via storeBindings in config:

// In tenant config
{
component: 'PropertyInfoCard',
storeBindings: [
{ prop: 'address', storePath: 'property.selectedAddress' },
{ prop: 'propertyId', storePath: 'property.propertyId' },
],
props: {
mapConfig: { interactive: true },
},
}

The SlotRenderer automatically resolves bindings at render time, injecting store values as component props.

Visibility Conditions

Components can be conditionally shown based on state:

{
component: 'PlanningStage',
showWhen: (state) => state.stage?.current === 'planning',
}

The condition is evaluated on every state change.

Data Flow Example

1. User selects an address in the UI
└─► onClick handler fires

2. Component calls setStore
└─► setStore('property.selectedAddress', '123 Main St', 'AddressSearch')

3. Store updates via Immer
└─► Immutable state update + persist to sessionStorage if enabled

4. SlotRenderer detects state change
└─► Re-evaluates showWhen conditions

5. Components re-render with new props
└─► storeBindings inject updated values

API Data Flow

For data from the backend:

1. Component needs data
└─► useQuery or fetch call

2. Request goes to /api/eai/*
└─► fetch('/api/eai/v3/projects', { credentials: 'include' })

3. BFF proxy adds auth token
└─► Authorization: Bearer <token>

4. Backend returns data
└─► Response proxied back to client

5. Component updates store
└─► setStore('projects.list', data, 'ProjectsPage')

6. UI reflects new data
└─► Components using useStoreValue('projects.list') re-render

Persistence

Slices with persist: true are saved to sessionStorage:

store: {
property: {
initialState: { selectedAddress: null },
persist: true, // Survives page refresh
},
ui: {
initialState: { sidebarOpen: true },
persist: false, // Resets on refresh
},
}
  • Survives page refresh within the same session
  • Cleared on tab close (sessionStorage behavior)
  • Slice-level control: Only persist what you need

DevTools Integration

Actions are named for debugging in Redux DevTools:

setStore('user.email', 'test@example.com', 'AddressLookup');
// In Redux DevTools: "set/user.email (AddressLookup)"

Enable the Redux DevTools browser extension to see all state changes.

API & Hooks