Skip to content

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.

Released under the MIT License.