React Signify Style Guide
Overview
This Style Guide provides coding conventions and best practices for using React Signify effectively and maintainably. Following these rules will help:
- More readable and maintainable code
- Avoid common mistakes
- Optimize application performance
- Effective team collaboration
Naming Conventions
1. Prefix "s" for Signify Variables
Rule: Always use prefix s
for Signify variables to distinguish from React state and other variables.
tsx
// ✅ Good: Clear Signify identification
const sUserProfile = signify(defaultProfile);
const sShoppingCart = signify([]);
const sAppConfig = signify(initialConfig);
const sCurrentUser = signify(null);
// ❌ Avoid: Unclear naming
const userProfile = signify(defaultProfile); // Looks like regular variable
const cart = signify([]); // Too generic
const config = signify(initialConfig); // Ambiguous
2. Prefix "ss" for Slice Variables
Rule: Use prefix ss
for slice to easily identify derived state.
tsx
const sUser = signify({
profile: { name: "John", age: 25 },
preferences: { theme: "dark", language: "en" },
permissions: ["read", "write"],
});
// ✅ Good: Clear slice identification
const ssUserName = sUser.slice((user) => user.profile.name);
const ssUserPreferences = sUser.slice((user) => user.preferences);
const ssIsAdmin = sUser.slice((user) => user.permissions.includes("admin"));
// ❌ Avoid: Confusing naming
const userName = sUser.slice((user) => user.profile.name); // Looks like regular variable
const sUserName = sUser.slice((user) => user.profile.name); // Confused with main signify
3. Descriptive and Consistent Naming
tsx
// ✅ Good: Descriptive and consistent
const sAuthenticationState = signify<
"idle" | "loading" | "authenticated" | "error"
>("idle");
const sUserNotifications = signify<Notification[]>([]);
const sFormValidationErrors = signify<Record<string, string>>({});
// ✅ Good: Domain-specific naming
const sInventoryItems = signify<Product[]>([]);
const sOrderProcessingStatus = signify<OrderStatus>("pending");
const sPaymentGatewayConfig = signify(defaultGatewayConfig);
// ❌ Avoid: Generic and ambiguous
const sData = signify(someData); // Too generic
const sState = signify(someState); // Too generic
const sInfo = signify(someInfo); // Too generic
Code Organization
1. Centralized Signify Declarations
Rule: Declare Signify instances in dedicated files or at the top-level of components.
tsx
// ✅ Good: Centralized store file
// stores/userStore.ts
export const sCurrentUser = signify<User | null>(null);
export const sUserPreferences = signify(defaultPreferences);
export const sUserPermissions = signify<string[]>([]);
// stores/appStore.ts
export const sAppConfig = signify(defaultConfig);
export const sNotifications = signify<Notification[]>([]);
export const sAppTheme = signify<"light" | "dark">("light");
// ✅ Good: Screen-level signify
// screens/ProductListScreen.tsx
const sProductFilters = signify(defaultFilters);
const sSelectedProducts = signify<string[]>([]);
export default function ProductListScreen() {
// Component logic
useEffect(() => {
// ✅ Reset when component unmounts
return () => {
sProductFilters.reset();
sSelectedProducts.reset();
};
}, []);
}
2. Proper File Structure
tsx
// ✅ Good: Organized import structure
import React, { useEffect } from "react";
import { signify } from "react-signify";
// Types
interface UserProfile {
name: string;
email: string;
preferences: UserPreferences;
}
// Signify declarations
const sUserProfile = signify<UserProfile | null>(null);
const sIsLoading = signify(false);
const sError = signify<string | null>(null);
// Slices
const ssUserName = sUserProfile.slice((user) => user?.name || "");
const ssUserEmail = sUserProfile.slice((user) => user?.email || "");
// Component
export default function UserProfileComponent() {
// Component logic
}
Performance Best Practices
1. Use .use()
intelligently
tsx
// ✅ Good: Selective subscription with picker
function UserProfile() {
const userName = sUser.use((user) => user.name); // Only re-render when name changes
const userEmail = sUser.use((user) => user.email); // Only re-render when email changes
return (
<div>
<h1>{userName}</h1>
<p>{userEmail}</p>
</div>
);
}
// ✅ Better: Use slices for better performance
const ssUserName = sUser.slice((user) => user.name);
const ssUserEmail = sUser.slice((user) => user.email);
function UserProfile() {
const userName = ssUserName.use(); // Optimized subscription
const userEmail = ssUserEmail.use(); // Optimized subscription
return (
<div>
<h1>{userName}</h1>
<p>{userEmail}</p>
</div>
);
}
// ❌ Avoid: Overuse of .use() causes unnecessary re-renders
function UserProfile() {
const user = sUser.use(); // Re-render when ANY field changes
return (
<div>
<h1>{user.name}</h1> // Can use slice instead
<p>{user.email}</p> // Can use slice instead
</div>
);
}
2. Choosing between Wrap and HardWrap
tsx
// ✅ Use Wrap: When you need data from parent component
function ProductCard({
productId,
onSelect,
}: {
productId: string;
onSelect: () => void;
}) {
return (
<sProduct.Wrap>
{(product) => (
<div onClick={onSelect}>
{/* Use prop from parent */}
<h3>{product.name}</h3>
<p>ID: {productId}</p> {/* Use prop from parent */}
</div>
)}
</sProduct.Wrap>
);
}
// ✅ Use HardWrap: When you only need data from Signify
function ProductDisplay() {
return (
<sProduct.HardWrap>
{(product) => (
<div>
<h3>{product.name}</h3> {/* Only use data from Signify */}
<p>${product.price}</p> {/* Not affected by parent re-render */}
<p>{product.description}</p>
</div>
)}
</sProduct.HardWrap>
);
}
3. Efficient State Management
tsx
// ✅ Good: Granular state management
const sUserName = signify("");
const sUserEmail = signify("");
const sUserAge = signify(0);
// Each field can update independently
// Components only re-render when the field they subscribe to changes
// ✅ Also good: Grouped related data with slices
const sUser = signify({ name: "", email: "", age: 0 });
const ssUserName = sUser.slice((user) => user.name);
const ssUserEmail = sUser.slice((user) => user.email);
const ssUserAge = sUser.slice((user) => user.age);
// ❌ Avoid: Overly granular for simple cases
const sUserFirstName = signify("");
const sUserLastName = signify("");
const sUserMiddleName = signify("");
// Better: const sUserFullName = signify({ first: '', last: '', middle: '' });
State Lifecycle Management
1. Proper Cleanup
tsx
// ✅ Good: Component-level cleanup
function ProductDetailsScreen({ productId }: { productId: string }) {
const sProduct = signify<Product | null>(null);
const sIsLoading = signify(false);
useEffect(() => {
// Load product data
sIsLoading.set(true);
loadProduct(productId).then((product) => {
sProduct.set(product);
sIsLoading.set(false);
});
// ✅ Cleanup when component unmounts
return () => {
sProduct.reset();
sIsLoading.reset();
};
}, [productId]);
// Component logic
}
2. Cache and Sync Key Management
tsx
// ✅ Good: Centralized sync keys
// constants/syncKeys.ts
export const SYNC_KEYS = {
USER_PREFERENCES: "user-prefs-v2",
SHOPPING_CART: "cart-v1",
APP_SETTINGS: "app-settings-v3",
NOTIFICATION_SETTINGS: "notifications-v1",
} as const;
// stores/userStore.ts
import { SYNC_KEYS } from "../constants/syncKeys";
export const sUserPreferences = signify(defaultPrefs, {
syncKey: SYNC_KEYS.USER_PREFERENCES,
cache: {
key: "user-preferences",
ttl: 7 * 24 * 60 * 60 * 1000, // 7 days
},
});
// ✅ Good: Environment-specific sync keys
const SYNC_KEY_PREFIX =
process.env.NODE_ENV === "development" ? "dev-" : "prod-";
export const sAppData = signify(initialData, {
syncKey: `${SYNC_KEY_PREFIX}app-data-v1`,
});
// ❌ Avoid: Hardcoded sync keys
export const sUserData = signify(userData, {
syncKey: "user-data", // May conflict between versions
});
Error Handling & Validation
1. Proper Error States
tsx
// ✅ Good: Comprehensive error handling
interface AsyncState<T> {
data: T | null;
loading: boolean;
error: string | null;
}
const sUserProfile = signify<AsyncState<UserProfile>>({
data: null,
loading: false,
error: null,
});
// Helper functions
const setLoading = (loading: boolean) => {
sUserProfile.set((prev) => {
prev.value.loading = loading;
if (loading) {
prev.value.error = null; // Clear error when starting loading
}
});
};
const setError = (error: string) => {
sUserProfile.set((prev) => {
prev.value.loading = false;
prev.value.error = error;
});
};
const setData = (data: UserProfile) => {
sUserProfile.set({
data,
loading: false,
error: null,
});
};
2. Validation with conditionUpdating
tsx
// ✅ Good: Input validation
const sUserAge = signify(0);
sUserAge.conditionUpdating((prev, curr) => {
// Business rules validation
if (curr < 0 || curr > 150) {
console.warn("Invalid age:", curr);
return false; // Block invalid updates
}
return true;
});
// ✅ Good: Complex object validation
const sUserProfile = signify<UserProfile>({ name: "", email: "", age: 0 });
sUserProfile.conditionUpdating((prev, curr) => {
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(curr.email)) {
console.warn("Invalid email format:", curr.email);
return false;
}
// Validate age range
if (curr.age < 13 || curr.age > 120) {
console.warn("Invalid age range:", curr.age);
return false;
}
// Validate name length
if (curr.name.trim().length < 2) {
console.warn("Name too short:", curr.name);
return false;
}
return true;
});
TypeScript Best Practices
1. Strong Typing
tsx
// ✅ Good: Explicit generic types
interface User {
id: string;
name: string;
email: string;
role: "admin" | "user" | "guest";
}
const sCurrentUser = signify<User | null>(null);
const sUsers = signify<User[]>([]);
const sUserRole = signify<User["role"]>("guest");
// ✅ Good: Union types cho states
type LoadingState = "idle" | "loading" | "success" | "error";
const sLoadingState = signify<LoadingState>("idle");
// ✅ Good: Generic helpers
function createAsyncSignify<T>(initialData: T) {
return signify<{
data: T;
loading: boolean;
error: string | null;
}>({
data: initialData,
loading: false,
error: null,
});
}
const sUserProfile = createAsyncSignify<UserProfile | null>(null);
const sProductList = createAsyncSignify<Product[]>([]);
2. Type-safe Slicing
tsx
// ✅ Good: Type-safe slice with proper typing
interface AppState {
user: User | null;
settings: AppSettings;
notifications: Notification[];
}
const sAppState = signify<AppState>(initialAppState);
// Type-safe slices
const ssCurrentUser = sAppState.slice((state): User | null => state.user);
const ssAppSettings = sAppState.slice((state): AppSettings => state.settings);
const ssNotificationCount = sAppState.slice(
(state): number => state.notifications.length
);
const ssUnreadNotifications = sAppState.slice((state): Notification[] =>
state.notifications.filter((n) => !n.read)
);
Watch vs Use Patterns
1. Use .watch() for Non-React Contexts
tsx
// ✅ Good: Use watch for side effects outside React
const sUserPreferences = signify(defaultPrefs);
// Service layer watching for changes
sUserPreferences.watch((prefs) => {
// Sync to backend
api.updateUserPreferences(prefs);
// Update analytics
analytics.track("preferences_changed", prefs);
// Update local storage
localStorage.setItem("prefs", JSON.stringify(prefs));
});
// ✅ Good: Integration with third-party libraries
const sTheme = signify<"light" | "dark">("light");
sTheme.watch((theme) => {
// Update CSS custom properties
document.documentElement.className = theme;
// Update chart library theme
Chart.defaults.theme = theme;
});
// ❌ Avoid: Using watch inside React components unnecessarily
function UserProfile() {
const user = sUser.use(); // ✅ Correct for React rendering
// ❌ Wrong: watch inside component
sUser.watch((user) => {
console.log("User changed:", user); // Use useEffect instead
});
}
2. Memory Management for Watch
tsx
// ✅ Good: Cleanup watch listeners
class UserService {
private unsubscribe: (() => void)[] = [];
constructor() {
// Store cleanup functions
this.unsubscribe.push(sUser.watch((user) => this.syncToBackend(user)));
this.unsubscribe.push(
sNotifications.watch((notifications) => this.updateBadge(notifications))
);
}
destroy() {
// ✅ Clean up all watchers
this.unsubscribe.forEach((cleanup) => cleanup());
this.unsubscribe = [];
}
}
// ✅ Good: Conditional watch cleanup
const setupUserWatcher = (userId: string) => {
const cleanup = sUser.watch((user) => {
if (user.id === userId) {
// Handle user-specific logic
}
});
return cleanup; // Return cleanup function
};
Advanced Configuration
1. Cache Strategy Best Practices
tsx
// ✅ Good: Strategic cache configuration
const sCacheConfig = {
// Short-term: Session data
USER_SESSION: {
type: "SessionStorage" as const,
key: "user-session-v1",
},
// Long-term: User preferences
USER_PREFERENCES: {
type: "LocalStorage" as const,
key: "user-prefs-v2",
},
// No cache: Sensitive data
PAYMENT_INFO: undefined, // No cache for security
};
// User session (cleared on tab close)
const sUserSession = signify(null, {
cache: sCacheConfig.USER_SESSION,
syncKey: "user-session",
});
// User preferences (persisted)
const sUserPreferences = signify(defaultPrefs, {
cache: sCacheConfig.USER_PREFERENCES,
syncKey: "user-prefs",
});
// Sensitive data (memory only)
const sPaymentInfo = signify(null); // No cache/sync
2. Sync Key Management
tsx
// ✅ Good: Environment-aware sync keys
const createSyncKey = (key: string) => {
const env = process.env.NODE_ENV;
const version = process.env.REACT_APP_VERSION || "v1";
return `${env}-${version}-${key}`;
};
export const SYNC_KEYS = {
USER_DATA: createSyncKey("user-data"),
APP_SETTINGS: createSyncKey("app-settings"),
CART: createSyncKey("shopping-cart"),
NOTIFICATIONS: createSyncKey("notifications"),
} as const;
// ✅ Good: Feature-specific sync
const sShoppingCart = signify<CartItem[]>([], {
syncKey: SYNC_KEYS.CART,
cache: {
type: "LocalStorage",
key: "cart-v1",
},
});
// ❌ Avoid: Hardcoded sync keys without versioning
const sUserData = signify(userData, {
syncKey: "user", // May cause conflicts between app versions
});
DevTool and Debugging
1. Development Environment Setup
tsx
// ✅ Good: Conditional DevTool usage
const sAppState = signify(initialState);
// Development debugging component
const AppStateDebugger = () => {
if (process.env.NODE_ENV !== "development") {
return null;
}
return (
<div style={{ position: "fixed", top: 0, right: 0, zIndex: 9999 }}>
<sAppState.DevTool />
</div>
);
};
// ✅ Good: Slice debugging
const ssUserProfile = sUser.slice((user) => user.profile);
const UserProfileDebugger = () =>
process.env.NODE_ENV === "development" ? <ssUserProfile.DevTool /> : null;
2. Performance Monitoring
tsx
// ✅ Good: Performance monitoring with watch
const sRenderCount = signify(0);
const setupPerformanceMonitoring = () => {
const startTime = Date.now();
sAppState.watch(() => {
const renderTime = Date.now() - startTime;
sRenderCount.set((prev) => prev.value + 1);
if (renderTime > 100) {
// Log slow renders
console.warn(`Slow render detected: ${renderTime}ms`);
}
});
};
// ✅ Good: Debug render frequency
const sDebugInfo = signify({
totalRenders: 0,
lastRenderTime: Date.now(),
slowRenders: 0,
});
sLargeState.watch(() => {
sDebugInfo.set((prev) => {
const now = Date.now();
const isSlowRender = now - prev.value.lastRenderTime > 16; // 60fps
prev.value.totalRenders += 1;
prev.value.lastRenderTime = now;
if (isSlowRender) prev.value.slowRenders += 1;
});
});
Testing Strategies
1. Unit Testing Signify Logic
tsx
// ✅ Good: Test signify behavior
describe("User Profile Signify", () => {
beforeEach(() => {
sUserProfile.reset(); // Always reset before tests
});
test("should update user name correctly", () => {
const initialUser = { name: "John", age: 25 };
sUserProfile.set(initialUser);
// Test state update
sUserProfile.set((prev) => {
prev.value.name = "Jane";
});
expect(sUserProfile.value.name).toBe("Jane");
expect(sUserProfile.value.age).toBe(25); // Unchanged
});
test("should handle conditional updates", () => {
sUserProfile.conditionUpdating((prev, curr) => curr.age >= 0);
sUserProfile.set({ name: "John", age: -5 });
expect(sUserProfile.value.age).not.toBe(-5); // Update blocked
});
});
2. Integration Testing with React
tsx
// ✅ Good: Test React integration
const TestComponent = () => {
const user = sUserProfile.use();
const userName = ssUserName.use(); // Test slice too
return (
<div>
<span data-testid="full-user">{JSON.stringify(user)}</span>
<span data-testid="user-name">{userName}</span>
</div>
);
};
test("should update component when signify changes", () => {
render(<TestComponent />);
act(() => {
sUserProfile.set({ name: "Alice", age: 30 });
});
expect(screen.getByTestId("user-name")).toHaveTextContent("Alice");
});
Complex State Patterns
1. Nested Object Management
tsx
// ✅ Good: Deep nested updates
interface AppState {
user: {
profile: { name: string; avatar: string };
settings: { theme: string; language: string };
};
ui: {
modals: { [key: string]: boolean };
loading: { [key: string]: boolean };
};
}
const sAppState = signify<AppState>(initialState);
// Helper functions for complex updates
const updateUserProfile = (updates: Partial<AppState["user"]["profile"]>) => {
sAppState.set((prev) => {
Object.assign(prev.value.user.profile, updates);
});
};
const toggleModal = (modalName: string, isOpen?: boolean) => {
sAppState.set((prev) => {
prev.value.ui.modals[modalName] =
isOpen ?? !prev.value.ui.modals[modalName];
});
};
const setLoading = (key: string, loading: boolean) => {
sAppState.set((prev) => {
prev.value.ui.loading[key] = loading;
});
};
2. Array Operations Best Practices
tsx
// ✅ Good: Efficient array operations
const sTodos = signify<Todo[]>([]);
// Add item
const addTodo = (todo: Todo) => {
sTodos.set((prev) => {
prev.value.push(todo); // Direct mutation in callback
});
};
// Remove item
const removeTodo = (id: string) => {
sTodos.set((prev) => {
const index = prev.value.findIndex((todo) => todo.id === id);
if (index !== -1) {
prev.value.splice(index, 1);
}
});
};
// Update item
const updateTodo = (id: string, updates: Partial<Todo>) => {
sTodos.set((prev) => {
const todo = prev.value.find((t) => t.id === id);
if (todo) {
Object.assign(todo, updates);
}
});
};
// ✅ Good: Optimized array slices
const ssCompletedTodos = sTodos.slice((todos) =>
todos.filter((t) => t.completed)
);
const ssPendingCount = sTodos.slice(
(todos) => todos.filter((t) => !t.completed).length
);
const ssTodoById = (id: string) =>
sTodos.slice((todos) => todos.find((t) => t.id === id));
Migration Patterns
1. From useState to Signify
tsx
// ❌ Before: Multiple useState
function UserComponent() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// Complex state management...
}
// ✅ After: Signify with organized state
const sUserState = signify({
data: null as User | null,
loading: false,
error: null as string | null,
});
function UserComponent() {
const { data: user, loading, error } = sUserState.use();
// Simplified state management
}
2. From Context to Signify
tsx
// ❌ Before: Complex Context setup
const UserContext = createContext();
const UserProvider = ({ children }) => {
const [user, setUser] = useState(null);
// Complex provider logic...
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
};
// ✅ After: Simple global state
// stores/userStore.ts
export const sCurrentUser = signify<User | null>(null);
// Any component can use directly
function AnyComponent() {
const user = sCurrentUser.use();
// Direct access, no provider needed
}
Summary Checklist
✅ Naming Conventions
- [ ] Use prefix
s
for signify variables - [ ] Use prefix
ss
for slice variables - [ ] Descriptive and consistent naming
- [ ] Avoid generic names like
sData
,sState
✅ Code Organization
- [ ] Centralize signify declarations
- [ ] Proper file structure and imports
- [ ] Group related signify instances
- [ ] Clear separation of concerns
✅ Performance
- [ ] Use slices for selective updates
- [ ] Choose between Wrap and HardWrap correctly
- [ ] Avoid overuse of
.use()
- [ ] Implement proper conditional updates/rendering
✅ Watch vs Use
- [ ] Use
.watch()
for non-React side effects - [ ] Proper cleanup for watch listeners
- [ ] Use
.use()
for React component rendering - [ ] Memory management for long-running watchers
✅ Advanced Configuration
- [ ] Strategic cache configuration (Session vs Local)
- [ ] Environment-aware sync keys
- [ ] Versioned configuration keys
- [ ] Security considerations for sensitive data
✅ Lifecycle Management
- [ ] Proper cleanup in useEffect
- [ ] Reset signify when component unmounts
- [ ] Centralized sync key management
- [ ] Environment-specific configurations
✅ Debugging & Monitoring
- [ ] Conditional DevTool usage in development
- [ ] Performance monitoring with watch
- [ ] Debug render frequency tracking
- [ ] Proper error logging and debugging
✅ Testing
- [ ] Unit tests for signify logic
- [ ] Integration tests with React components
- [ ] Always reset state before tests
- [ ] Test conditional updates and rendering
✅ Complex State Management
- [ ] Efficient nested object updates
- [ ] Optimized array operations
- [ ] Helper functions for complex updates
- [ ] Strategic slice creation for performance
✅ Error Handling
- [ ] Comprehensive error states
- [ ] Input validation with conditionUpdating
- [ ] Graceful error recovery
- [ ] Proper logging and debugging
✅ TypeScript
- [ ] Strong typing with explicit generics
- [ ] Type-safe slicing
- [ ] Avoid
any
types - [ ] Proper interface definitions
💡 Remember: Good style isn't just about syntax - it's about creating maintainable, performant, and scalable code that your team can work with efficiently. React Signify provides powerful tools - use them wisely to build robust applications.