Best Practices
Ecosistema completo para organización y gestión de contenido con interfaces espaciales
Flash Persistence - Best Practices
Recommended patterns and practices for building robust applications with Flash Persistence.
Table of Contents
- Key Naming Conventions
- State Organization
- Performance Optimization
- Type Safety
- Testing
- Security
- Common Pitfalls
Key Naming Conventions
Use Descriptive, Hierarchical Keys
// ❌ Bad - Flat, unclear
const state1 = autoFlashState({ key: 'data', initialValue: {} });
const state2 = autoFlashState({ key: 'stuff', initialValue: {} });
// ✅ Good - Hierarchical, clear
const userProfile = autoFlashState({
key: 'profile',
database: 'user-data',
collection: 'profiles',
initialValue: {}
});
const appSettings = autoFlashState({
key: 'settings',
database: 'app-data',
collection: 'config',
initialValue: {}
});
Follow Consistent Patterns
// ✅ Consistent naming scheme
const patterns = {
// User-specific data
userPreferences: 'user-data:preferences:*',
userProfile: 'user-data:profile:*',
userHistory: 'user-data:history:*',
// App-level data
appState: 'app-data:state:*',
appConfig: 'app-data:config:*',
appCache: 'app-data:cache:*',
// Session data
sessionData: 'session:data:*',
sessionTemp: 'session:temp:*'
};
Avoid Special Characters
// ❌ Bad - Special characters cause issues
autoFlashState({ key: 'user:profile', initialValue: {} });
autoFlashState({ key: 'app/state', initialValue: {} });
// ✅ Good - Use database/collection/namespace instead
autoFlashState({
key: 'profile',
database: 'user',
initialValue: {}
});
State Organization
Service-Based State Management
// ✅ Centralize related state in services
@Injectable({ providedIn: 'root' })
export class UserPreferencesService {
// Persistent state
private state = autoFlashState({
key: 'preferences',
database: 'user-data',
collection: 'settings',
initialValue: {
theme: 'dark',
language: 'en',
notifications: true
}
});
// Read-only signals for consumers
theme = computed(() => this.state().theme);
language = computed(() => this.state().language);
notifications = computed(() => this.state().notifications);
// Actions
setTheme(theme: string) {
this.state.update(s => ({ ...s, theme }));
}
setLanguage(language: string) {
this.state.update(s => ({ ...s, language }));
}
toggleNotifications() {
this.state.update(s => ({ ...s, notifications: !s.notifications }));
}
}
Feature-Based Organization
// features/cart/cart.service.ts
@Injectable({ providedIn: 'root' })
export class CartService {
private cart = autoFlashState<CartItem[]>({
key: 'items',
database: 'ecommerce',
collection: 'cart',
initialValue: []
});
items = this.cart.asReadonly();
itemCount = computed(() => this.cart().length);
total = computed(() =>
this.cart().reduce((sum, item) => sum + item.price, 0)
);
addItem(item: CartItem) { /* ... */ }
removeItem(id: string) { /* ... */ }
}
Composition Over Inheritance
// ✅ Compose services
@Injectable({ providedIn: 'root' })
export class CheckoutService {
private cart = inject(CartService);
private user = inject(UserService);
private payments = inject(PaymentService);
private checkoutState = autoFlashState({
key: 'checkout',
database: 'ecommerce',
collection: 'checkout',
initialValue: {
step: 1,
shippingAddress: null,
paymentMethod: null
}
});
// Use composition to access cart, user, payment state
canCheckout = computed(() =>
this.cart.itemCount() > 0 && this.user.isAuthenticated()
);
}
Performance Optimization
Use Computed Signals for Derived State
// ❌ Bad - Recomputes on every access
class CartService {
cart = autoFlashState<CartItem[]>({ key: 'cart', initialValue: [] });
getTotal() {
return this.cart().reduce((sum, item) => sum + item.price, 0);
}
}
// ✅ Good - Computed once, cached
class CartService {
cart = autoFlashState<CartItem[]>({ key: 'cart', initialValue: [] });
total = computed(() =>
this.cart().reduce((sum, item) => sum + item.price, 0)
);
}
Batch Updates
// ❌ Bad - Multiple persistence triggers
function addMultipleItems(items: CartItem[]) {
items.forEach(item => {
cart.update(c => [...c, item]); // Each triggers persistence!
});
}
// ✅ Good - Single persistence trigger
function addMultipleItems(items: CartItem[]) {
cart.update(c => [...c, ...items]);
}
Use TTL for Temporary Data
// ✅ Automatic cleanup
const searchResults = autoFlashState({
key: 'search-results',
initialValue: [],
ttl: 5 * 60 * 1000 // 5 minutes
});
const sessionData = autoFlashState({
key: 'session',
initialValue: null,
ttl: 30 * 60 * 1000 // 30 minutes
});
Optimize Large Arrays
// ❌ Bad - Stores entire array
const largeList = autoFlashState({
key: 'list',
initialValue: [] // Could be thousands of items
});
// ✅ Good - Paginate or limit
const recentItems = autoFlashState({
key: 'recent',
initialValue: [] // Keep only last 50
});
function addRecentItem(item: any) {
recentItems.update(items => [item, ...items].slice(0, 50));
}
Debounce User Input
@Component({
template: `
<input [ngModel]="searchQuery()"
(ngModelChange)="updateSearch($event)" />
`
})
export class SearchComponent {
searchQuery = autoFlashState({
key: 'search-query',
initialValue: ''
});
private updateDebounced = debounce((query: string) => {
this.searchQuery.set(query);
}, 300);
updateSearch(query: string) {
this.updateDebounced(query);
}
}
Type Safety
Use Generic Types
// ✅ Fully typed
interface UserPreferences {
theme: 'light' | 'dark';
language: 'en' | 'es' | 'fr';
notifications: boolean;
}
const preferences = autoFlashState<UserPreferences>({
key: 'preferences',
initialValue: {
theme: 'dark',
language: 'en',
notifications: true
}
});
// TypeScript enforces valid values
preferences.set({
theme: 'light', // ✅ Valid
language: 'invalid', // ❌ Type error
notifications: true
});
Create Type-Safe Builders
// ✅ Type-safe builder pattern
class PreferencesBuilder {
static create<T extends Record<string, any>>(
key: string,
initialValue: T
) {
return autoFlashState<T>({
key,
database: 'user-data',
collection: 'preferences',
initialValue
});
}
}
// Usage with full type inference
const prefs = PreferencesBuilder.create('my-prefs', {
theme: 'dark',
autoSave: true
});
prefs().theme; // ✅ Type: string
prefs().autoSave; // ✅ Type: boolean
Avoid Type Assertions
// ❌ Bad - Loses type safety
const state = autoFlashState({
key: 'data',
initialValue: {} as any
});
// ✅ Good - Explicit interface
interface AppData {
loaded: boolean;
error: string | null;
data: any[];
}
const state = autoFlashState<AppData>({
key: 'data',
initialValue: {
loaded: false,
error: null,
data: []
}
});
Testing
Test State Persistence
describe('CartService', () => {
let service: CartService;
let storage: GgStorage;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [provideFlashPersistence()]
});
service = TestBed.inject(CartService);
storage = TestBed.inject(GgStorage);
});
it('should persist cart items', async () => {
const item = { id: '1', name: 'Test', price: 10 };
service.addItem(item);
// Wait for debounce + persistence
await new Promise(resolve => setTimeout(resolve, 400));
// Verify persistence
const stored = await storage.get('ecommerce:cart:items');
expect(stored).toContainEqual(item);
});
it('should restore cart on reload', async () => {
const items = [
{ id: '1', name: 'Item 1', price: 10 },
{ id: '2', name: 'Item 2', price: 20 }
];
// Store directly
await storage.put('ecommerce:cart:items', items);
// Recreate service (simulates page reload)
service = TestBed.inject(CartService);
// Should restore from storage
expect(service.cart()).toEqual(items);
});
});
Mock Flash Persistence
// test-utils/flash-persistence-mock.ts
export function provideMockFlashPersistence() {
return {
provide: FlashStateHydratorService,
useValue: {
updateState: jasmine.createSpy('updateState'),
getInstantState: jasmine.createSpy('getInstantState'),
clearState: jasmine.createSpy('clearState')
}
};
}
// Usage in tests
TestBed.configureTestingModule({
providers: [provideMockFlashPersistence()]
});
Test Computed Signals
it('should compute total correctly', () => {
service.cart.set([
{ id: '1', price: 10, quantity: 2 },
{ id: '2', price: 20, quantity: 1 }
]);
expect(service.total()).toBe(40); // (10*2) + (20*1)
});
Security
Avoid Storing Sensitive Data
// ❌ Bad - Never store in browser storage
const passwords = autoFlashState({
key: 'passwords',
initialValue: []
});
const creditCards = autoFlashState({
key: 'payment-methods',
initialValue: []
});
// ✅ Good - Store only non-sensitive identifiers
const authToken = autoFlashState({
key: 'auth-token',
initialValue: null,
ttl: 60 * 60 * 1000 // 1 hour, auto-expire
});
const paymentMethodIds = autoFlashState({
key: 'saved-payment-ids',
initialValue: [] // Just IDs, actual data stays server-side
});
Sanitize User Input
// ✅ Sanitize before storing
function updateUserProfile(input: any) {
const sanitized = {
name: sanitizeString(input.name),
bio: sanitizeString(input.bio),
website: sanitizeUrl(input.website)
};
userProfile.set(sanitized);
}
Use TTL for Session Data
// ✅ Auto-expire sensitive session data
const sessionData = autoFlashState({
key: 'session',
initialValue: null,
ttl: 30 * 60 * 1000 // 30 minutes
});
Encrypt Sensitive Data
// ✅ Encrypt before storing (if absolutely necessary)
import { encrypt, decrypt } from './crypto-utils';
const encryptedData = autoFlashState({
key: 'encrypted',
initialValue: null
});
function storeSecure(data: any) {
const encrypted = encrypt(JSON.stringify(data));
encryptedData.set(encrypted);
}
function retrieveSecure(): any {
const encrypted = encryptedData();
if (!encrypted) return null;
return JSON.parse(decrypt(encrypted));
}
Common Pitfalls
1. Forgetting Initial Values
// ❌ Bad - Undefined initial value
const state = autoFlashState({
key: 'data',
initialValue: undefined // Will cause issues
});
// ✅ Good - Explicit initial value
const state = autoFlashState({
key: 'data',
initialValue: null // or {}, [], etc.
});
2. Mutating State Directly
// ❌ Bad - Mutates signal value
const cart = autoFlashState<CartItem[]>({
key: 'cart',
initialValue: []
});
cart().push(newItem); // ❌ Doesn't trigger updates!
// ✅ Good - Immutable update
cart.update(items => [...items, newItem]);
3. Not Using Computed for Derived Values
// ❌ Bad - Recomputes every time
function getExpensiveValue() {
return cart().map(item => {
// Expensive computation
return processItem(item);
});
}
// ✅ Good - Computed once
const expensiveValue = computed(() =>
cart().map(item => processItem(item))
);
4. Over-Persisting
// ❌ Bad - Persists every mouse move
@HostListener('mousemove', ['$event'])
onMouseMove(event: MouseEvent) {
mousePosition.set({ x: event.clientX, y: event.clientY });
// This triggers persistence every frame!
}
// ✅ Good - Don't persist transient UI state
mousePosition = signal({ x: 0, y: 0 });
// Or use debounce/throttle if persistence is needed
5. Circular Dependencies
// ❌ Bad - Circular effect
const a = autoFlashState({ key: 'a', initialValue: 0 });
const b = autoFlashState({ key: 'b', initialValue: 0 });
effect(() => {
b.set(a() + 1); // Sets b based on a
});
effect(() => {
a.set(b() + 1); // Sets a based on b - circular!
});
// ✅ Good - Unidirectional data flow
const a = autoFlashState({ key: 'a', initialValue: 0 });
const b = computed(() => a() + 1); // Derived, not stored
6. Ignoring TTL for Cache
// ❌ Bad - Cache never expires
const apiCache = autoFlashState({
key: 'api-cache',
initialValue: {}
});
// ✅ Good - Set appropriate TTL
const apiCache = autoFlashState({
key: 'api-cache',
initialValue: {},
ttl: 5 * 60 * 1000 // 5 minutes
});
Architecture Patterns
Repository Pattern
@Injectable({ providedIn: 'root' })
export class UserRepository {
private cache = autoFlashState<User[]>({
key: 'users-cache',
database: 'cache',
initialValue: [],
ttl: 5 * 60 * 1000
});
async getUsers(): Promise<User[]> {
const cached = this.cache();
if (cached.length > 0) return cached;
const users = await this.api.fetchUsers();
this.cache.set(users);
return users;
}
}
Facade Pattern
@Injectable({ providedIn: 'root' })
export class AppFacade {
private userService = inject(UserService);
private cartService = inject(CartService);
private prefsService = inject(PreferencesService);
// Unified interface
readonly user = this.userService.currentUser;
readonly cart = this.cartService.items;
readonly theme = this.prefsService.theme;
// Unified actions
updateTheme(theme: string) {
this.prefsService.setTheme(theme);
}
addToCart(item: CartItem) {
this.cartService.addItem(item);
}
}
Next Steps
- 🔧 Advanced Usage - Complex scenarios
- 📚 API Reference - Complete API
- 📖 Overview - Architecture
- 🚀 Quick Start - Get started
Questions? Join our GitHub Discussions.