Skip to content

Using Method .set()

Overview

The .set() method is the primary way to update the value of a Signify instance. It supports two modes of operation:

  1. Direct value assignment - Replace the entire value
  2. Callback-based modification - Modify the current value using a callback function
tsx
// Direct assignment - replaces the entire value
signify.set(newValue);

// Callback modification - modify prev.value directly
signify.set(prev => {
  prev.value.property = newValue; // Modify prev.value directly
  // IMPORTANT: Do NOT return anything from the callback
});

Core Concepts

The .set() method provides two ways to update your state:

  1. Replace the entire value - Simple and direct
  2. Modify the existing value - Powerful and flexible
  3. Smart update detection - Only re-renders when necessary
  4. Safe state changes - Prevents common mutation errors
  5. Predictable behavior - Consistent results every time
tsx
// Type signature
type TSetterCallback<T> = (prev: { value: T }) => void;
set(value: T | TSetterCallback<T>): void

How It Works

1. Direct Value Setting

tsx
import { signify } from 'react-signify';

const sCount = signify(0);
const sMessage = signify('Hello');
const sUser = signify(null);

// ✅ Set primitive values
sCount.set(10);
sMessage.set('World');
sUser.set({ id: 1, name: 'John' });

2. Callback Function Setting

tsx
const sCounter = signify(0);

// ✅ Increment counter
sCounter.set(prev => {
  prev.value += 1;
});

// ✅ Multiply by 2
sCounter.set(prev => {
  prev.value *= 2;
});

3. Smart Update Detection

Signify is intelligent about when to trigger updates - it only re-renders when your data actually changes:

tsx
const sData = signify('initial');
const sObject = signify({ count: 0 });

// String values
sData.set('initial');    // ❌ No update (same value)
sData.set('changed');    // ✅ Update triggered (different value)
sData.set('changed');    // ❌ No update (same value again)

// Object values  
sObject.set({ count: 0 });     // ❌ No update (same content)
sObject.set({ count: 1 });     // ✅ Update triggered (different content)

// Callback modifications
sObject.set(prev => {
  prev.value.count += 1;       // ✅ Modification always updates
});

Key insight: Signify compares the actual content, not just references, so identical data won't cause unnecessary re-renders.

Primitive Values

Numbers

tsx
const sCount = signify(0);

// ✅ Direct assignment
sCount.set(5);
sCount.set(-10);
sCount.set(3.14);

// ✅ Mathematical operations
sCount.set(prev => {
  prev.value += 10;        // Increment
  prev.value -= 5;         // Decrement  
  prev.value *= 2;         // Multiply
  prev.value /= 3;         // Divide
  prev.value = Math.pow(prev.value, 2); // Power
});

// ✅ Conditional updates
sCount.set(prev => {
  if (prev.value < 100) {
    prev.value += 1;
  }
});

Strings

tsx
const sMessage = signify('Hello');

// ✅ Direct assignment
sMessage.set('World');
sMessage.set('');

// ✅ String operations
sMessage.set(prev => {
  prev.value += ' World';           // Concatenation
  prev.value = prev.value.toUpperCase(); // Transform
  prev.value = prev.value.trim();   // Clean up
});

// ✅ Template strings
const sUserGreeting = signify('Hello');
sUserGreeting.set(prev => {
  prev.value = `Hello, ${userName}!`;
});

Booleans

tsx
const sIsVisible = signify(false);
const sLoading = signify(false);

// ✅ Direct assignment
sIsVisible.set(true);
sLoading.set(false);

// ✅ Toggle
sIsVisible.set(prev => {
  prev.value = !prev.value;
});

// ✅ Conditional logic
sLoading.set(prev => {
  prev.value = apiCallInProgress && !hasError;
});

Objects

Simple Objects

tsx
const sUser = signify({
  name: 'John',
  age: 25,
  email: 'john@example.com'
});

// ✅ Update single property
sUser.set(prev => {
  prev.value.age += 1;
});

// ✅ Update multiple properties
sUser.set(prev => {
  prev.value.name = 'John Doe';
  prev.value.email = 'john.doe@example.com';
});

// ✅ Complete replacement
sUser.set({
  name: 'Jane',
  age: 30,
  email: 'jane@example.com'
});

Nested Objects

tsx
const sUserProfile = signify({
  personal: {
    firstName: 'John',
    lastName: 'Doe'
  },
  preferences: {
    theme: 'light',
    language: 'en'
  },
  settings: {
    notifications: true,
    privacy: {
      showEmail: false,
      showPhone: true
    }
  }
});

// ✅ Update nested properties
sUserProfile.set(prev => {
  prev.value.personal.firstName = 'Jane';
  prev.value.preferences.theme = 'dark';
  prev.value.settings.privacy.showEmail = true;
});

// ✅ Update entire nested object
sUserProfile.set(prev => {
  prev.value.preferences = {
    theme: 'auto',
    language: 'vi'
  };
});

Optional Properties

tsx
interface User {
  id: number;
  name: string;
  avatar?: string;
  profile?: {
    bio?: string;
    location?: string;
  };
}

const sUser = signify<User>({
  id: 1,
  name: 'John'
});

// ✅ Add optional properties
sUser.set(prev => {
  prev.value.avatar = 'https://example.com/avatar.jpg';
  prev.value.profile = {
    bio: 'Software Developer',
    location: 'Hanoi'
  };
});

// ✅ Update optional nested properties
sUser.set(prev => {
  if (!prev.value.profile) {
    prev.value.profile = {};
  }
  prev.value.profile.bio = 'Updated bio';
});

Arrays

Basic Array Operations

tsx
const sItems = signify([]);

// ✅ Add items
sItems.set(prev => {
  prev.value.push('new item');
  prev.value.push('another item');
});

// ✅ Remove items
sItems.set(prev => {
  prev.value.pop();                           // Remove last
  prev.value.shift();                         // Remove first
  prev.value.splice(1, 1);                    // Remove at index
  prev.value = prev.value.filter(item => item !== 'target'); // Filter
});

// ✅ Replace entire array
sItems.set(['item1', 'item2', 'item3']);

Complex Array Operations

tsx
interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

const sTodos = signify<Todo[]>([]);

// ✅ Add new todo
const addTodo = (text: string) => {
  sTodos.set(prev => {
    prev.value.push({
      id: Date.now(),
      text,
      completed: false
    });
  });
};

// ✅ Toggle todo completion
const toggleTodo = (id: number) => {
  sTodos.set(prev => {
    const todo = prev.value.find(t => t.id === id);
    if (todo) {
      todo.completed = !todo.completed;
    }
  });
};

// ✅ Update todo text
const updateTodo = (id: number, newText: string) => {
  sTodos.set(prev => {
    const todo = prev.value.find(t => t.id === id);
    if (todo) {
      todo.text = newText;
    }
  });
};

// ✅ Remove todo
const removeTodo = (id: number) => {
  sTodos.set(prev => {
    prev.value = prev.value.filter(t => t.id !== id);
  });
};

// ✅ Bulk operations
const markAllCompleted = () => {
  sTodos.set(prev => {
    prev.value.forEach(todo => {
      todo.completed = true;
    });
  });
};

Array Sorting and Filtering

tsx
const sProducts = signify([
  { id: 1, name: 'Laptop', price: 1000, category: 'electronics' },
  { id: 2, name: 'Book', price: 20, category: 'books' }
]);

// ✅ Sort by price
const sortByPrice = () => {
  sProducts.set(prev => {
    prev.value.sort((a, b) => a.price - b.price);
  });
};

// ✅ Sort by name
const sortByName = () => {
  sProducts.set(prev => {
    prev.value.sort((a, b) => a.name.localeCompare(b.name));
  });
};

// ✅ Filter by category
const filterByCategory = (category: string) => {
  sProducts.set(prev => {
    prev.value = prev.value.filter(p => p.category === category);
  });
};

Advanced Patterns

1. Conditional Updates

tsx
const sScore = signify(0);

// ✅ Only allow positive increments
const safeIncrement = (amount: number) => {
  sScore.set(prev => {
    if (amount > 0 && prev.value + amount <= 1000) {
      prev.value += amount;
    }
  });
};

// ✅ Validation before update
const sEmail = signify('');
const updateEmail = (newEmail: string) => {
  sEmail.set(prev => {
    if (newEmail.includes('@') && newEmail.length > 5) {
      prev.value = newEmail;
    }
  });
};

2. Batched Updates

tsx
const sUserForm = signify({
  name: '',
  email: '',
  age: 0,
  errors: {}
});

// ✅ Update multiple fields at once
const updateUserForm = (updates: Partial<UserForm>) => {
  sUserForm.set(prev => {
    Object.keys(updates).forEach(key => {
      prev.value[key] = updates[key];
    });
    
    // Clear related errors
    prev.value.errors = {};
  });
};

// Usage
updateUserForm({
  name: 'John Doe',
  email: 'john@example.com',
  age: 30
});

3. State Machines

tsx
type LoadingState = 'idle' | 'loading' | 'success' | 'error';

const sApiState = signify({
  status: 'idle' as LoadingState,
  data: null,
  error: null
});

// ✅ State transitions
const startLoading = () => {
  sApiState.set(prev => {
    prev.value.status = 'loading';
    prev.value.error = null;
  });
};

const setSuccess = (data: any) => {
  sApiState.set(prev => {
    prev.value.status = 'success';
    prev.value.data = data;
    prev.value.error = null;
  });
};

const setError = (error: string) => {
  sApiState.set(prev => {
    prev.value.status = 'error';
    prev.value.error = error;
    prev.value.data = null;
  });
};

4. Computed Properties

tsx
const sCart = signify({
  items: [] as CartItem[],
  discountPercent: 0,
  tax: 0.1
});

// ✅ Update computed fields when relevant data changes
const updateCartTotals = () => {
  sCart.set(prev => {
    const subtotal = prev.value.items.reduce((sum, item) => 
      sum + (item.price * item.quantity), 0
    );
    
    const discountAmount = subtotal * (prev.value.discountPercent / 100);
    const taxAmount = (subtotal - discountAmount) * prev.value.tax;
    
    prev.value.subtotal = subtotal;
    prev.value.discountAmount = discountAmount;
    prev.value.taxAmount = taxAmount;
    prev.value.total = subtotal - discountAmount + taxAmount;
  });
};

// Call after any item changes
const addToCart = (product: Product) => {
  sCart.set(prev => {
    prev.value.items.push({
      id: Date.now(),
      productId: product.id,
      name: product.name,
      price: product.price,
      quantity: 1
    });
  });
  
  updateCartTotals(); // Recalculate totals
};

Understanding Update Patterns

Learning when and how updates occur helps you write more predictable code.

Direct Assignment Pattern

tsx
const sUser = signify({ 
  profile: { name: 'John', age: 30 },
  preferences: { theme: 'dark' }
});

// Same content = no update
sUser.set({ 
  profile: { name: 'John', age: 30 },
  preferences: { theme: 'dark' }
});

// Different content = update triggered
sUser.set({ 
  profile: { name: 'John', age: 31 },
  preferences: { theme: 'dark' }
});

Callback Modification Pattern

tsx
const sCounter = signify(0);

// Direct value calculation
const increment = (amount) => {
  sCounter.set(sCounter.value + amount);
};

// Callback-based modification
const safeIncrement = (amount) => {
  sCounter.set(prev => {
    prev.value += amount;
  });
};

Learning tip: Callbacks are great for modifications, direct assignment is perfect for replacements.

Performance Considerations

1. Minimize Update Frequency

tsx
const sSearchQuery = signify('');

// ❌ Update on every keystroke
const handleInputChange = (e) => {
  sSearchQuery.set(e.target.value); // Too frequent
};

// ✅ Debounced updates
const [localQuery, setLocalQuery] = useState('');

useEffect(() => {
  const timer = setTimeout(() => {
    sSearchQuery.set(localQuery);
  }, 300);
  
  return () => clearTimeout(timer);
}, [localQuery]);

const handleInputChange = (e) => {
  setLocalQuery(e.target.value); // Local state first
};

2. Working with Complex Objects

When dealing with large or complex objects, use simple and direct approaches:

tsx
const sLargeObject = signify({
  metadata: { /* large object */ },
  data: { /* another large object */ }
});

// ✅ Simple and efficient - modify what you need
sLargeObject.set(prev => {
  prev.value.newProperty = 'value';        // Direct modification
  prev.value.metadata.updated = new Date(); // Update nested properties
});

// ❌ Avoid unnecessary complexity
sLargeObject.set(prev => {
  const cloned = JSON.parse(JSON.stringify(prev.value)); // Overcomplicated
  cloned.newProperty = 'value';
  prev.value = cloned;
});

// ❌ Don't return objects from callbacks
sLargeObject.set(prev => {
  return { ...prev.value, newProperty: 'value' }; // Wrong pattern
});

Learning tip: Keep it simple - just modify prev.value directly in callbacks.

tsx
const sTheme = signify('light');
const sLanguage = signify('en');
const sNotifications = signify(true);

// ❌ Multiple separate updates
const updateUserPreferences = (prefs) => {
  sTheme.set(prefs.theme);           // Update 1
  sLanguage.set(prefs.language);     // Update 2  
  sNotifications.set(prefs.notifications); // Update 3
};

// ✅ Single object with all preferences
const sUserPreferences = signify({
  theme: 'light',
  language: 'en', 
  notifications: true
});

const updateUserPreferences = (prefs) => {
  sUserPreferences.set(prev => {     // Single update
    Object.assign(prev.value, prefs);
  });
};

Error Handling

1. Safe Property Access

tsx
const sUser = signify(null);

// ✅ Safe nested property updates
const updateUserProfile = (profileData) => {
  sUser.set(prev => {
    if (!prev.value) {
      prev.value = { profile: {} };
    }
    
    if (!prev.value.profile) {
      prev.value.profile = {};
    }
    
    Object.assign(prev.value.profile, profileData);
  });
};

2. Validation

tsx
const sFormData = signify({
  email: '',
  age: 0
});

// ✅ Validate before updating
const updateFormField = (field: string, value: any) => {
  sFormData.set(prev => {
    // Validation logic
    if (field === 'email' && !value.includes('@')) {
      console.warn('Invalid email format');
      return; // Don't update
    }
    
    if (field === 'age' && (value < 0 || value > 150)) {
      console.warn('Invalid age');
      return; // Don't update
    }
    
    prev.value[field] = value;
  });
};

3. Transaction-like Updates

tsx
const sBankAccount = signify({
  balance: 1000,
  transactions: []
});

// ✅ Atomic operations
const transfer = (amount: number, toAccount: string) => {
  sBankAccount.set(prev => {
    if (prev.value.balance < amount) {
      throw new Error('Insufficient funds');
    }
    
    // Both operations succeed or fail together
    prev.value.balance -= amount;
    prev.value.transactions.push({
      id: Date.now(),
      type: 'transfer',
      amount: -amount,
      to: toAccount,
      timestamp: new Date()
    });
  });
};

Integration with Forms

1. Controlled Components

tsx
const sLoginForm = signify({
  username: '',
  password: '',
  remember: false
});

function LoginForm() {
  const form = sLoginForm.use();
  
  return (
    <form>
      <input
        type="text"
        value={form.username}
        onChange={(e) => sLoginForm.set(prev => {
          prev.value.username = e.target.value;
        })}
      />
      
      <input
        type="password"  
        value={form.password}
        onChange={(e) => sLoginForm.set(prev => {
          prev.value.password = e.target.value;
        })}
      />
      
      <input
        type="checkbox"
        checked={form.remember}
        onChange={(e) => sLoginForm.set(prev => {
          prev.value.remember = e.target.checked;
        })}
      />
    </form>
  );
}

2. Form Actions

tsx
const sRegistrationForm = signify({
  fields: {
    email: '',
    password: '',
    confirmPassword: ''
  },
  errors: {},
  isSubmitting: false
});

// ✅ Form action creators
export const formActions = {
  updateField: (field: string, value: string) => {
    sRegistrationForm.set(prev => {
      prev.value.fields[field] = value;
      
      // Clear field error when user types
      if (prev.value.errors[field]) {
        delete prev.value.errors[field];
      }
    });
  },
  
  setError: (field: string, message: string) => {
    sRegistrationForm.set(prev => {
      prev.value.errors[field] = message;
    });
  },
  
  clearErrors: () => {
    sRegistrationForm.set(prev => {
      prev.value.errors = {};
    });
  },
  
  setSubmitting: (isSubmitting: boolean) => {
    sRegistrationForm.set(prev => {
      prev.value.isSubmitting = isSubmitting;
    });
  },
  
  reset: () => {
    sRegistrationForm.set({
      fields: { email: '', password: '', confirmPassword: '' },
      errors: {},
      isSubmitting: false
    });
  }
};

Best Practices

✅ Do's

tsx
// ✅ Use callback for modifications
sCounter.set(prev => {
  prev.value += 1;
});

// ✅ Batch related updates
sUser.set(prev => {
  prev.value.name = newName;
  prev.value.email = newEmail;
  prev.value.updatedAt = new Date();
});

// ✅ Validate before updating
sAge.set(prev => {
  if (newAge >= 0 && newAge <= 150) {
    prev.value = newAge;
  }
});

// ✅ Use direct assignment for complete replacement
sItems.set([]);  // Clear array
sUser.set(null); // Clear user

❌ Don'ts

tsx
// ❌ Don't mutate the value directly without using .set()
const currentValue = sData.value;
currentValue.property = 'modified'; // Won't trigger updates!

// ❌ Don't return values from callback
sData.set(prev => {
  return { ...prev.value, newProp: 'value' }; // Wrong! This won't work
});

// ✅ Correct - modify prev.value directly
sData.set(prev => {
  prev.value.newProp = 'value'; // Direct modification works
});

// ❌ Don't forget null/undefined safety
sUser.set(prev => {
  prev.value.name = 'John'; // Error if prev.value is null!
});

// ✅ Correct - handle null values safely
sUser.set(prev => {
  if (prev.value) {
    prev.value.name = 'John';
  }
});

// ❌ Don't update too frequently without debouncing
onChange={(e) => {
  sQuery.set(e.target.value); // Triggers on every keystroke!
}}

// ❌ Don't try to access the signify instance from within callback
sData.set(prev => {
  sData.value.property = 'wrong'; // Confusing and unreliable
  prev.value.property = 'correct'; // Clear and correct
});

TypeScript Support

tsx
interface User {
  id: number;
  name: string;
  preferences?: {
    theme: 'light' | 'dark';
    language: string;
  };
}

const sUser = signify<User | null>(null);

// ✅ Type-safe updates
sUser.set({
  id: 1,
  name: 'John',
  preferences: {
    theme: 'dark',    // ✅ Valid theme value
    language: 'en'
  }
});

// ✅ Type-safe property access
sUser.set(prev => {
  if (prev.value) {  // ✅ Null check required
    prev.value.name = 'Jane';
    
    if (!prev.value.preferences) {
      prev.value.preferences = {
        theme: 'light',
        language: 'en'
      };
    }
    
    prev.value.preferences.theme = 'dark'; // ✅ Type-safe
  }
});

Debugging Tips

1. Tracking State Changes

tsx
const sDebugData = signify({ count: 0 });

// ✅ Monitor all changes with .watch()
if (process.env.NODE_ENV === 'development') {
  sDebugData.watch(value => {
    console.log('Value changed to:', value);
  });
}

// ✅ Debug your updates
const updateWithLogging = (newValue) => {
  console.log('About to update with:', newValue);
  
  sDebugData.set(prev => {
    console.log('Current value in callback:', prev.value);
    prev.value.count = newValue;
    console.log('Modified to:', prev.value);
  });
  
  console.log('Final value:', sDebugData.value);
};

2. Understanding Update Behavior

tsx
const sTestData = signify({ items: [] });

// ✅ Check if your data is actually different
const testUpdate = (newValue) => {
  const current = sTestData.value;
  console.log('Current:', current);
  console.log('New:', newValue);
  
  sTestData.set(newValue);
  
  console.log('Updated to:', sTestData.value);
};

// ✅ Test with objects
testUpdate({ items: [] });        // May not update if same content
testUpdate({ items: [1] });       // Should update
testUpdate({ items: [1, 2] });    // Should update

2. Validation in Development

tsx
const sValidatedData = signify({});

const updateData = (key: string, value: any) => {
  sValidatedData.set(prev => {
    // ✅ Development-only validation
    if (process.env.NODE_ENV === 'development') {
      if (typeof key !== 'string') {
        console.warn('Key must be string:', key);
      }
      if (value === undefined) {
        console.warn('Value should not be undefined');
      }
    }
    
    prev.value[key] = value;
  });
};

Summary

The .set() method is your primary tool for updating Signify values and triggering re-renders:

Two Ways to Update

  1. Direct assignment - Replace the entire value:

    tsx
    signify.set(newValue)
  2. Callback modification - Modify the existing value:

    tsx
    signify.set(prev => { 
      prev.value.property = newValue 
    })

Key Behaviors to Remember

  • Smart updates: Only re-renders when content actually changes
  • Safe modifications: Callbacks protect you from common mutation errors
  • Consistent results: The same input always produces the same output
  • Type safety: Full TypeScript support helps catch errors early

When to Use Each Pattern

Use direct assignment for:

tsx
signify.set(newValue)  // ✅ Complete replacement
signify.set(null)      // ✅ Reset/clear
signify.set([])        // ✅ Replace entire array

Use callbacks for:

tsx
signify.set(prev => {  // ✅ Modify existing data
  prev.value.count += 1;
  prev.value.updated = new Date();
})

Learning Principles

  • Callbacks work with { value: T } - Access your data through prev.value
  • Modify, don't return - Change prev.value directly in callbacks
  • One source of truth - Always use .set() to change state
  • Test your understanding - Use console.log to see what's happening

Best Practice

Start with direct assignment for simple cases, then learn callbacks for complex modifications. Both patterns are powerful when used appropriately.


💡 Remember: .set() is your gateway to state changes - master it to master Signify!

Released under the MIT License.