Building an Admin Dashboard with SoulState
Admin dashboards are complex applications with numerous data tables, forms, real-time updates, and intricate user interactions. SoulState's performance-first architecture and modular design make it ideal for managing state in these demanding environments.
Architecture Overview
Modular Store Structure
src/stores/
├── userStore.ts # User management
├── productStore.ts # Product catalog
├── orderStore.ts # Order processing
├── uiStore.ts # Theme, sidebar, notifications
└── index.ts # Combined exports
1. Domain-Specific Stores
Create focused stores for each business domain to maintain separation of concerns and optimize performance.
User Management Store
// stores/userStore.ts
import { createStore } from 'soulstate';
export interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'editor' | 'viewer';
status: 'active' | 'inactive';
lastLogin: Date;
}
export interface UserState {
users: User[];
selectedUser: User | null;
loading: boolean;
error: string | null;
filters: {
role: string;
status: string;
search: string;
};
}
export const userStore = createStore<UserState>({
users: [],
selectedUser: null,
loading: false,
error: null,
filters: {
role: '',
status: '',
search: ''
}
});
export const userActions = {
// Async actions
fetchUsers: async () => {
userStore.set({ loading: true, error: null });
try {
const response = await fetch('/api/users');
const users = await response.json();
userStore.set({ users, loading: false });
} catch (error) {
userStore.set({ error: 'Failed to fetch users', loading: false });
}
},
// Sync actions
setSelectedUser: (user: User | null) => {
userStore.set({ selectedUser: user });
},
updateUser: (userId: string, updates: Partial<User>) => {
userStore.set(state => ({
users: state.users.map(user =>
user.id === userId ? { ...user, ...updates } : user
)
}));
},
setFilters: (filters: Partial<UserState['filters']>) => {
userStore.set(state => ({
filters: { ...state.filters, ...filters }
}));
}
};
Product Catalog Store
// stores/productStore.ts
import { createStore } from 'soulstate';
export interface Product {
id: string;
name: string;
price: number;
stock: number;
category: string;
status: 'active' | 'archived';
}
export const productStore = createStore({
products: [] as Product[],
loading: false,
categories: [] as string[]
});
export const productActions = {
fetchProducts: async (category?: string) => {
productStore.set({ loading: true });
const url = category ? `/api/products?category=${category}` : '/api/products';
const response = await fetch(url);
const products = await response.json();
productStore.set({ products, loading: false });
},
updateStock: (productId: string, newStock: number) => {
productStore.set(state => ({
products: state.products.map(product =>
product.id === productId ? { ...product, stock: newStock } : product
)
}));
}
};
2. Efficient Data Tables with Surgical Selectors
Use precise selectors to ensure components only re-render when their specific data changes.
User Table Component
// components/UserTable.tsx
import { useStore } from 'soulstate/react';
import { userStore, userActions } from '../stores/userStore';
import { shallow } from 'soulstate/utils';
export function UserTable() {
// Subscribe to filtered users and loading state
const { filteredUsers, loading } = useStore(
userStore,
(state) => {
let users = state.users;
// Apply filters
if (state.filters.role) {
users = users.filter(user => user.role === state.filters.role);
}
if (state.filters.status) {
users = users.filter(user => user.status === state.filters.status);
}
if (state.filters.search) {
const searchLower = state.filters.search.toLowerCase();
users = users.filter(user =>
user.name.toLowerCase().includes(searchLower) ||
user.email.toLowerCase().includes(searchLower)
);
}
return { filteredUsers: users, loading: state.loading };
},
shallow // Use shallow comparison for object selection
);
if (loading) {
return <div className="loading">Loading users...</div>;
}
return (
<table className="user-table">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Role</th>
<th>Status</th>
<th>Last Login</th>
</tr>
</thead>
<tbody>
{filteredUsers.map(user => (
<UserTableRow key={user.id} userId={user.id} />
))}
</tbody>
</table>
);
}
// Individual row component for optimal performance
function UserTableRow({ userId }: { userId: string }) {
// Each row subscribes only to its specific user data
const user = useStore(
userStore,
(state) => state.users.find(u => u.id === userId)
);
if (!user) return null;
return (
<tr>
<td>{user.name}</td>
<td>{user.email}</td>
<td>
<select
value={user.role}
onChange={(e) => userActions.updateUser(userId, { role: e.target.value as any })}
>
<option value="admin">Admin</option>
<option value="editor">Editor</option>
<option value="viewer">Viewer</option>
</select>
</td>
<td>
<span className={`status-badge ${user.status}`}>
{user.status}
</span>
</td>
<td>{new Date(user.lastLogin).toLocaleDateString()}</td>
</tr>
);
}
3. Real-time Updates with Store Subscriptions
Integrate WebSocket connections for live data updates.
// services/websocketService.ts
import { userStore, userActions } from '../stores/userStore';
import { productStore, productActions } from '../stores/productStore';
export class WebSocketService {
private ws: WebSocket | null = null;
connect() {
this.ws = new WebSocket('ws://localhost:8080/realtime');
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'USER_UPDATED':
userActions.updateUser(data.userId, data.updates);
break;
case 'PRODUCT_STOCK_UPDATED':
productActions.updateStock(data.productId, data.stock);
break;
case 'NEW_ORDER':
// Handle new order notification
break;
}
};
this.ws.onclose = () => {
// Attempt reconnection
setTimeout(() => this.connect(), 5000);
};
}
disconnect() {
this.ws?.close();
}
}
export const websocketService = new WebSocketService();
4. Global UI State Management
Manage theme, sidebar, and notifications in a dedicated UI store.
// stores/uiStore.ts
import { createStore } from 'soulstate';
export interface Notification {
id: string;
type: 'success' | 'error' | 'warning' | 'info';
title: string;
message: string;
timestamp: Date;
}
export interface UIState {
theme: 'light' | 'dark';
sidebarOpen: boolean;
currentPage: string;
notifications: Notification[];
modals: {
userCreate: boolean;
userEdit: boolean;
productImport: boolean;
};
}
export const uiStore = createStore<UIState>({
theme: 'light',
sidebarOpen: true,
currentPage: 'dashboard',
notifications: [],
modals: {
userCreate: false,
userEdit: false,
productImport: false
}
});
export const uiActions = {
toggleTheme: () => {
uiStore.set(state => ({
theme: state.theme === 'light' ? 'dark' : 'light'
}));
},
toggleSidebar: () => {
uiStore.set(state => ({ sidebarOpen: !state.sidebarOpen }));
},
addNotification: (notification: Omit<Notification, 'id' | 'timestamp'>) => {
const newNotification: Notification = {
...notification,
id: Math.random().toString(36).substr(2, 9),
timestamp: new Date()
};
uiStore.set(state => ({
notifications: [...state.notifications, newNotification]
}));
// Auto-remove after 5 seconds
setTimeout(() => {
uiActions.removeNotification(newNotification.id);
}, 5000);
},
removeNotification: (id: string) => {
uiStore.set(state => ({
notifications: state.notifications.filter(n => n.id !== id)
}));
},
openModal: (modal: keyof UIState['modals']) => {
uiStore.set(state => ({
modals: { ...state.modals, [modal]: true }
}));
},
closeModal: (modal: keyof UIState['modals']) => {
uiStore.set(state => ({
modals: { ...state.modals, [modal]: false }
}));
}
};
5. Performance Optimizations
Memoized Selectors for Complex Data
// stores/selectors.ts
import { userStore } from './userStore';
// Memoized selector for user statistics
export const getUserStats = () => {
const users = userStore.get().users;
return {
total: users.length,
active: users.filter(u => u.status === 'active').length,
admins: users.filter(u => u.role === 'admin').length,
editors: users.filter(u => u.role === 'editor').length
};
};
// Component using memoized selector
export function UserStats() {
const stats = useStore(userStore, (state) => {
const users = state.users;
return {
total: users.length,
active: users.filter(u => u.status === 'active').length,
admins: users.filter(u => u.role === 'admin').length
};
});
return (
<div className="stats-grid">
<div className="stat-card">
<h3>Total Users</h3>
<span className="stat-value">{stats.total}</span>
</div>
<div className="stat-card">
<h3>Active Users</h3>
<span className="stat-value">{stats.active}</span>
</div>
<div className="stat-card">
<h3>Admins</h3>
<span className="stat-value">{stats.admins}</span>
</div>
</div>
);
}
Batch Updates for Better Performance
// Optimized batch update example
export const userActions = {
bulkUpdateUsers: (updates: Array<{ userId: string; updates: Partial<User> }>) => {
// Single set call for multiple updates
userStore.set(state => ({
users: state.users.map(user => {
const update = updates.find(u => u.userId === user.id);
return update ? { ...user, ...update.updates } : user;
})
}));
},
// Batch multiple related updates
createUserWithProfile: async (userData: Omit<User, 'id'>) => {
userStore.set({ loading: true });
try {
const user = await api.createUser(userData);
// Batch both updates
userStore.set(state => ({
users: [...state.users, { ...user, id: user.id }],
loading: false
}));
uiActions.addNotification({
type: 'success',
title: 'User Created',
message: `User ${user.name} was created successfully`
});
} catch (error) {
userStore.set({ loading: false });
uiActions.addNotification({
type: 'error',
title: 'Creation Failed',
message: 'Failed to create user'
});
}
}
};
6. Store Integration and Initialization
// stores/index.ts
export { userStore, userActions } from './userStore';
export { productStore, productActions } from './productStore';
export { uiStore, uiActions } from './uiStore';
// App initialization
export const initializeStores = async () => {
// Load initial data
await Promise.all([
userActions.fetchUsers(),
productActions.fetchProducts()
]);
// Start real-time services
websocketService.connect();
};
// Cleanup on app unmount
export const cleanupStores = () => {
websocketService.disconnect();
};
✅
Performance Benefits
This architecture ensures that:
- ✅ User table rows only re-render when their specific user data changes
- ✅ Filter changes don't cause unnecessary re-renders in unaffected components
- ✅ Real-time updates are efficiently propagated to relevant components
- ✅ UI state changes are isolated and predictable
💡
Best Practices
For large admin dashboards:
- Use multiple focused stores instead of one giant store
- Leverage
shallowequality for object selections - Implement row-level components for large tables
- Batch related updates together
- Use precise selectors to minimize re-renders