# Project Structure
Understand how a Dynamic Framework project is organized and best practices to keep it scalable and maintainable.
# Base Structure
A typical Dynamic Framework widget follows this structure:
my-widget/
├── src/ # Source code
│ ├── components/ # Reusable components
│ ├── config/ # Configuration files
│ ├── locales/ # Translation files (i18n)
│ ├── providers/ # React context providers
│ ├── services/ # API services
│ ├── store/ # Zustand stores (UI state)
│ ├── styles/ # SCSS styles
│ ├── types/ # TypeScript type definitions
│ ├── utils/ # Utility functions
│ ├── App.tsx # Root component
│ └── main.tsx # Entry point
├── tests/ # Test files
│ └── setup.ts # Test configuration
├── public/ # Static files
│ └── index.html # HTML template
├── .env.example # Environment variables template
├── .eslintrc.js # ESLint configuration
├── tsconfig.json # TypeScript configuration
├── vite.config.ts # Vite configuration
├── package.json # Dependencies and scripts
└── README.md # Project documentation
# Main Directories
# /src/components/
Reusable widget components with barrel exports:
components/
├── index.ts # Barrel export
├── ErrorBoundary.tsx # Error boundary wrapper
├── DataStateWrapper.tsx # Loading/error/empty states handler
├── LoadingState.tsx # Loading skeleton variants
├── ErrorState.tsx # Error UI with retry
├── EmptyState.tsx # Empty data UI
└── AccountCard/
├── AccountCard.tsx
├── AccountCard.test.tsx
└── index.ts
# /src/config/
Application configuration:
config/
├── widgetConfig.ts # Widget configuration from Liquid
├── i18nConfig.ts # i18next setup
└── liquidConfig.ts # LiquidJS parser initialization
# /src/services/
API communication layer following the repository pattern:
services/
├── api/
│ └── client.ts # Axios HTTP client with interceptors
├── repositories/ # Data access layer
│ ├── accountRepository.ts
│ └── transactionRepository.ts
└── hooks/ # TanStack Query hooks
├── useAccounts.ts
└── useTransactions.ts
# /src/store/
UI state management with Zustand:
store/
└── useUIStore.ts # UI state (filters, modals, selections)
Zustand for UI State Only
Use Zustand exclusively for UI state (filters, modals, active tabs). Server data should be managed with TanStack Query, not Zustand.
# /src/providers/
React context providers:
providers/
└── QueryProvider.tsx # TanStack Query configuration
# /src/types/
Centralized TypeScript definitions:
types/
└── index.ts # All type definitions
Type naming conventions:
Entity- Domain typesApiEntity- API response mappingCreateEntityData/UpdateEntityData- Payload typesEntityFilters- Filter parameters
# /src/locales/
Internationalization files:
locales/
├── en.json # English translations
└── es.json # Spanish translations
# /src/styles/
Widget styles:
styles/
└── base.scss # Widget-specific styles
# Configuration Files
# package.json
{
"name": "my-widget",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "vite",
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"lint": "eslint .",
"push": "npm run build && npx @modyo/cli@latest push"
},
"dependencies": {
"@dynamic-framework/ui-react": "^2.0.0",
"@tanstack/react-query": "^5.60.0",
"axios": "^1.13.0",
"i18next": "^24.0.0",
"liquidjs": "^10.24.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-i18next": "^16.0.0",
"zustand": "^5.0.0"
},
"devDependencies": {
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.5.0",
"eslint": "^9.0.0",
"sass-embedded": "^1.93.0",
"typescript": "^5.9.0",
"vite": "^7.0.0",
"vitest": "^3.0.0"
},
"engines": {
"node": ">=22.0.0"
}
}
# vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import svgr from 'vite-plugin-svgr';
export default defineConfig({
plugins: [react(), svgr()],
css: {
preprocessorOptions: {
scss: {
quietDeps: true,
silenceDeprecations: ['legacy-js-api'],
},
},
},
build: {
outDir: 'build',
assetsDir: '',
rollupOptions: {
output: {
entryFileNames: 'main.js',
chunkFileNames: '[name].[hash].chunk.js',
assetFileNames: (assetInfo) => {
if (assetInfo.name?.endsWith('.css')) {
return 'main.css';
}
return '[name].[hash][extname]';
},
},
},
chunkSizeWarningLimit: 2000,
minify: 'esbuild',
},
test: {
globals: true,
environment: 'jsdom',
setupFiles: './tests/setup.ts',
},
});
# .env.example
# API Configuration
VITE_API_BASE_URL=https://api.example.com
# Feature Flags
VITE_ENABLE_DEVTOOLS=true
Vite Environment Variables
Vite uses the VITE_ prefix for environment variables. Access them via import.meta.env.VITE_*.
# tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true
},
"include": ["src"]
}
# Best Practices
# 1. Component Organization
Simple component:
AccountCard.tsx # Component with collocated styles/tests if small
Complex component:
AccountCard/
├── AccountCard.tsx # Main component
├── AccountCard.test.tsx # Tests
└── index.ts # Public export
# 2. Naming Conventions
- Components: PascalCase (
AccountCard.tsx) - Hooks: camelCase with
useprefix (useAccounts.ts) - Stores: camelCase with
useprefix (useUIStore.ts) - Utils: camelCase (
formatCurrency.ts) - Types: PascalCase (
Account,ApiAccount)
# 3. Organized Imports
// 1. React and external libraries
import { useState } from 'react';
// 2. Dynamic Framework components
import { DButton, DCard, DIcon } from '@dynamic-framework/ui-react';
// 3. Internal modules (hooks, stores, types)
import { useAccounts } from '../services/hooks/useAccounts';
import { useUIStore } from '../store/useUIStore';
import type { Account } from '../types';
// 4. Styles
import '../styles/base.scss';
# 4. State Management Strategy
UI State (Zustand):
// store/useUIStore.ts
import { create } from 'zustand';
interface UIState {
selectedAccountId: string | null;
isModalOpen: boolean;
setSelectedAccount: (id: string | null) => void;
toggleModal: () => void;
}
export const useUIStore = create<UIState>((set) => ({
selectedAccountId: null,
isModalOpen: false,
setSelectedAccount: (id) => set({ selectedAccountId: id }),
toggleModal: () => set((state) => ({ isModalOpen: !state.isModalOpen })),
}));
Server State (TanStack Query + Repository):
// services/repositories/accountRepository.ts
import { api } from '../api/client';
import type { Account } from '../../types';
export async function getAccounts(signal?: AbortSignal): Promise<Account[]> {
const response = await api.get('/accounts', { signal });
return response.data;
}
// services/hooks/useAccounts.ts
import { useQuery } from '@tanstack/react-query';
import { getAccounts } from '../repositories/accountRepository';
export function useAccounts() {
return useQuery({
queryKey: ['accounts'],
queryFn: ({ signal }) => getAccounts(signal),
});
}
# 5. Separation of Concerns
- Components: Presentation and UI interactions only
- Providers: Context configuration (Query, i18n)
- Services: API communication
- Store: UI state only (not server data)
- Utils: Pure utility functions
- Types: TypeScript definitions
# Entry Point Pattern
# main.tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { DContextProvider } from '@dynamic-framework/ui-react';
import App from './App';
import './config/i18nConfig';
import './styles/base.scss';
const container = document.getElementById('root');
if (container) {
createRoot(container).render(
<StrictMode>
<DContextProvider>
<App />
</DContextProvider>
</StrictMode>
);
}
# App.tsx
import { QueryProvider } from './providers/QueryProvider';
import { ErrorBoundary } from './components';
function App() {
return (
<QueryProvider>
<ErrorBoundary>
{/* Widget content */}
</ErrorBoundary>
</QueryProvider>
);
}
export default App;
# Testing
# Test Structure
tests/
├── setup.ts # Vitest setup (jsdom, mocks)
src/
├── components/
│ └── AccountCard.test.tsx # Collocated component tests
├── utils/
│ └── formatters.test.ts # Collocated util tests
# Test Example
// src/components/AccountCard.test.tsx
import { render, screen } from '@testing-library/react';
import { AccountCard } from './AccountCard';
describe('AccountCard', () => {
it('displays account name', () => {
render(<AccountCard name="Savings" balance={1000} />);
expect(screen.getByText('Savings')).toBeInTheDocument();
});
});
# Resources
- Storybook (opens new window) - Interactive component catalog
- NPM Package (opens new window) - Package details
- Vite Documentation (opens new window) - Build tool documentation