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:
- Direct value assignment - Replace the entire value
- Callback-based modification - Modify the current value using a callback function
// 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:
- Replace the entire value - Simple and direct
- Modify the existing value - Powerful and flexible
- Smart update detection - Only re-renders when necessary
- Safe state changes - Prevents common mutation errors
- Predictable behavior - Consistent results every time
// Type signature
type TSetterCallback<T> = (prev: { value: T }) => void;
set(value: T | TSetterCallback<T>): void
How It Works
1. Direct Value Setting
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
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:
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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:
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.
3. Batch Related Updates
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
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
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
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
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
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
// ✅ 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
// ❌ 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
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
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
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
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
Direct assignment - Replace the entire value:
tsxsignify.set(newValue)
Callback modification - Modify the existing value:
tsxsignify.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:
signify.set(newValue) // ✅ Complete replacement
signify.set(null) // ✅ Reset/clear
signify.set([]) // ✅ Replace entire array
Use callbacks for:
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 throughprev.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!