lifecycleMethod
Refract provides several lifecycle methods to help you manage side effects, handle component mounting and unmounting, and control when effects run. These work similarly to React's lifecycle methods but with Refract's reactive approach.
Reference
The main lifecycle methods available through the lens object are:
- useEffect() - Runs side effects with dependency tracking
- useFlash() - Runs effects once after component renders
- useMemo() - Memoizes expensive calculations
- useCallback() - Memoizes callback functions
useEffect
Runs side effects in response to component state changes or prop updates. This is your go-to method for data fetching, subscriptions, and cleanup.
lens.useEffect(effectFunction, dependencies);
Parameters
- effectFunction: A function that runs the side effect. Can return a cleanup function
- dependencies: An array of values that trigger the effect when they change
Basic usage
const DataComponent = createComponent(({ lens, userId }) => {
const user = lens.useRefraction(null);
const loading = lens.useRefraction(true);
// Effect runs when userId changes
lens.useEffect(() => {
const fetchUser = async () => {
loading.set(true);
try {
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
user.set(userData);
} catch (error) {
console.error('Failed to fetch user:', error);
} finally {
loading.set(false);
}
};
fetchUser();
}, [userId]);
return loading.value ? <div>Loading...</div> : <div>{user.value?.name}</div>;
});
Effect with cleanup
Return a cleanup function to handle subscriptions, timers, or other resources:
const TimerComponent = createComponent(({ lens }) => {
const seconds = lens.useRefraction(0);
lens.useEffect(() => {
const interval = setInterval(() => {
seconds.set(current => current + 1);
}, 1000);
// Cleanup function
return () => {
clearInterval(interval);
};
}, []); // Empty dependencies = run once on mount
return <div>Timer: {seconds.value}s</div>;
});
Multiple effects
You can use multiple effects in the same component:
const UserDashboard = createComponent(({ lens, userId }) => {
const user = lens.useRefraction(null);
const posts = lens.useRefraction([]);
const online = lens.useRefraction(false);
// Effect 1: Fetch user data
lens.useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(user.set);
}, [userId]);
// Effect 2: Fetch user's posts
lens.useEffect(() => {
fetch(`/api/users/${userId}/posts`)
.then(res => res.json())
.then(posts.set);
}, [userId]);
// Effect 3: Track online status
lens.useEffect(() => {
const handleOnline = () => online.set(true);
const handleOffline = () => online.set(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return (
<div>
<div className={`status ${online.value ? 'online' : 'offline'}`}>
{online.value ? 'Online' : 'Offline'}
</div>
{user.value && (
<div>
<h1>{user.value.name}</h1>
<p>{posts.value.length} posts</p>
</div>
)}
</div>
);
});
Conditional effects
Effects only run when their dependencies actually change:
const SearchComponent = createComponent(({ lens }) => {
const query = lens.useRefraction('');
const results = lens.useRefraction([]);
const loading = lens.useRefraction(false);
// This effect only runs when query changes and is not empty
lens.useEffect(() => {
if (!query.value.trim()) {
results.set([]);
return;
}
loading.set(true);
const searchTimeout = setTimeout(async () => {
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(query.value)}`);
const data = await response.json();
results.set(data.results);
} catch (error) {
console.error('Search failed:', error);
} finally {
loading.set(false);
}
}, 300); // Debounce search
return () => clearTimeout(searchTimeout);
}, [query.value]);
return (
<div>
<input
value={query.value}
onChange={(e) => query.set(e.target.value)}
placeholder="Search..."
/>
{loading.value && <div>Searching...</div>}
<ul>
{results.value.map(result => (
<li key={result.id}>{result.title}</li>
))}
</ul>
</div>
);
});
useFlash
Runs an effect exactly once after the component renders. Perfect for animations, focus management, or one-time setup that needs the DOM to be ready.
lens.useFlash(effectFunction);
Parameters
- effectFunction: A function that runs once after render
Animation example
const FadeInComponent = createComponent(({ lens, children }) => {
const elementRef = lens.useRef();
lens.useFlash(() => {
const element = elementRef.current;
if (element) {
element.style.opacity = '0';
element.style.transition = 'opacity 0.5s ease-in-out';
// Trigger fade-in
requestAnimationFrame(() => {
element.style.opacity = '1';
});
}
});
return (
<div ref={elementRef}>
{children}
</div>
);
});
Focus management
const AutoFocusInput = createComponent(({ lens, ...props }) => {
const inputRef = lens.useRef();
lens.useFlash(() => {
inputRef.current?.focus();
});
return <input ref={inputRef} {...props} />;
});
Integration setup
const AnalyticsTracker = createComponent(({ lens, pageName }) => {
lens.useFlash(() => {
// Track page view once after component mounts
if (typeof gtag !== 'undefined') {
gtag('event', 'page_view', {
page_title: pageName,
page_location: window.location.href
});
}
});
return null; // This component doesn't render anything
});
useMemo
Memoizes expensive calculations so they only run when their dependencies change. Use this to optimize performance when you have computationally expensive operations.
const memoizedValue = lens.useMemo(computeFunction, dependencies);
Parameters
- computeFunction: A function that returns the value to memoize
- dependencies: An array of values that trigger recalculation
Expensive calculations
const DataVisualization = createComponent(({ lens, rawData, filterCriteria }) => {
// This expensive calculation only runs when rawData or filterCriteria change
const processedData = lens.useMemo(() => {
console.log('Processing data...'); // You'll see this only when needed
return rawData
.filter(item => item.category === filterCriteria.category)
.map(item => ({
...item,
normalizedValue: (item.value - filterCriteria.min) / (filterCriteria.max - filterCriteria.min)
}))
.sort((a, b) => b.normalizedValue - a.normalizedValue);
}, [rawData, filterCriteria]);
return (
<div>
<h2>Data Visualization ({processedData.length} items)</h2>
{processedData.map(item => (
<div key={item.id} className="data-item">
<span>{item.name}</span>
<div
className="bar"
style={{ width: `${item.normalizedValue * 100}%` }}
/>
</div>
))}
</div>
);
});
Complex derived state
const ShoppingCart = createComponent(({ lens }) => {
const items = lens.useRefraction([
{ id: 1, name: 'Laptop', price: 999, quantity: 1, taxRate: 0.08 },
{ id: 2, name: 'Mouse', price: 25, quantity: 2, taxRate: 0.08 },
{ id: 3, name: 'Book', price: 15, quantity: 3, taxRate: 0.0 } // Books are tax-free
]);
const discount = lens.useRefraction(0.1); // 10% discount
// Memoize complex calculations
const cartSummary = lens.useMemo(() => {
const subtotal = items.value.reduce(
(sum, item) => sum + (item.price * item.quantity),
0
);
const taxableAmount = items.value.reduce(
(sum, item) => sum + (item.price * item.quantity * item.taxRate),
0
);
const discountAmount = subtotal * discount.value;
const finalTotal = subtotal + taxableAmount - discountAmount;
return {
subtotal: subtotal.toFixed(2),
taxAmount: taxableAmount.toFixed(2),
discountAmount: discountAmount.toFixed(2),
total: finalTotal.toFixed(2),
itemCount: items.value.reduce((sum, item) => sum + item.quantity, 0)
};
}, [items.value, discount.value]);
return (
<div>
<h2>Shopping Cart</h2>
{items.value.map(item => (
<div key={item.id} className="cart-item">
{item.name} x {item.quantity} = ${(item.price * item.quantity).toFixed(2)}
</div>
))}
<div className="cart-summary">
<p>Items: {cartSummary.itemCount}</p>
<p>Subtotal: ${cartSummary.subtotal}</p>
<p>Tax: ${cartSummary.taxAmount}</p>
<p>Discount: -${cartSummary.discountAmount}</p>
<p><strong>Total: ${cartSummary.total}</strong></p>
</div>
</div>
);
});
useCallback
Memoizes callback functions to prevent unnecessary re-renders of child components. This is especially useful when passing callbacks to child components.
const memoizedCallback = lens.useCallback(callbackFunction, dependencies);
Parameters
- callbackFunction: The function to memoize
- dependencies: An array of values that trigger callback recreation
Preventing unnecessary re-renders
const TodoList = createComponent(({ lens }) => {
const todos = lens.useRefraction([
{ id: 1, text: 'Learn Refract', completed: false },
{ id: 2, text: 'Build an app', completed: false }
]);
// Without useCallback, these functions would be recreated on every render
const handleToggle = lens.useCallback((id) => {
todos.update(current =>
current.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
}, [todos]); // Only recreate when todos reference changes
const handleDelete = lens.useCallback((id) => {
todos.update(current => current.filter(todo => todo.id !== id));
}, [todos]);
const handleEdit = lens.useCallback((id, newText) => {
todos.update(current =>
current.map(todo =>
todo.id === id ? { ...todo, text: newText } : todo
)
);
}, [todos]);
return (
<div>
<h2>Todo List</h2>
{todos.value.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
onDelete={handleDelete}
onEdit={handleEdit}
/>
))}
</div>
);
});
// Child component won't re-render unnecessarily thanks to memoized callbacks
const TodoItem = createComponent(({ lens, todo, onToggle, onDelete, onEdit }) => {
const editing = lens.useRefraction(false);
const editText = lens.useRefraction(todo.text);
const handleSaveEdit = () => {
onEdit(todo.id, editText.value);
editing.set(false);
};
const handleCancelEdit = () => {
editText.set(todo.text);
editing.set(false);
};
return (
<div className="todo-item">
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
{editing.value ? (
<div>
<input
value={editText.value}
onChange={(e) => editText.set(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleSaveEdit();
if (e.key === 'Escape') handleCancelEdit();
}}
/>
<button onClick={handleSaveEdit}>Save</button>
<button onClick={handleCancelEdit}>Cancel</button>
</div>
) : (
<div>
<span
className={todo.completed ? 'completed' : ''}
onDoubleClick={() => editing.set(true)}
>
{todo.text}
</span>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</div>
)}
</div>
);
});
Event handlers with parameters
const ButtonGroup = createComponent(({ lens, onButtonClick }) => {
const buttons = ['Save', 'Cancel', 'Delete'];
// Create memoized handlers for each button
const createHandler = lens.useCallback((buttonName) => {
return () => onButtonClick(buttonName);
}, [onButtonClick]);
return (
<div className="button-group">
{buttons.map(button => (
<button
key={button}
onClick={createHandler(button)}
className={`btn btn-${button.toLowerCase()}`}
>
{button}
</button>
))}
</div>
);
});
Lifecycle patterns
Component mounting and unmounting
const LifecycleExample = createComponent(({ lens, userId }) => {
const mounted = lens.useRef(false);
const data = lens.useRefraction(null);
// On mount
lens.useEffect(() => {
mounted.current = true;
console.log('Component mounted');
// Cleanup on unmount
return () => {
mounted.current = false;
console.log('Component unmounted');
};
}, []);
// Data fetching with mount check
lens.useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(`/api/users/${userId}`);
const result = await response.json();
// Only update state if component is still mounted
if (mounted.current) {
data.set(result);
}
} catch (error) {
if (mounted.current) {
console.error('Fetch error:', error);
}
}
};
fetchData();
}, [userId]);
return <div>{data.value?.name || 'Loading...'}</div>;
});
Combining lifecycle methods
const ComplexComponent = createComponent(({ lens, config }) => {
const data = lens.useRefraction([]);
const loading = lens.useRefraction(false);
const error = lens.useRefraction(null);
const elementRef = lens.useRef();
// Memoize expensive calculation
const processedData = lens.useMemo(() => {
return data.value
.filter(item => item.status === 'active')
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
.slice(0, config.maxItems || 10);
}, [data.value, config.maxItems]);
// Memoize callback
const handleItemClick = lens.useCallback((item) => {
console.log('Item clicked:', item);
config.onItemClick?.(item);
}, [config.onItemClick]);
// Data fetching effect
lens.useEffect(() => {
const fetchData = async () => {
loading.set(true);
error.set(null);
try {
const response = await fetch(config.apiEndpoint);
const result = await response.json();
data.set(result);
} catch (err) {
error.set(err.message);
} finally {
loading.set(false);
}
};
fetchData();
}, [config.apiEndpoint]);
// Animation effect (runs once after render)
lens.useFlash(() => {
const element = elementRef.current;
if (element) {
element.style.opacity = '0';
element.style.transform = 'translateY(20px)';
element.style.transition = 'all 0.3s ease';
requestAnimationFrame(() => {
element.style.opacity = '1';
element.style.transform = 'translateY(0)';
});
}
});
if (loading.value) return <div>Loading...</div>;
if (error.value) return <div>Error: {error.value}</div>;
return (
<div ref={elementRef}>
<h2>Data List ({processedData.length} items)</h2>
<ul>
{processedData.map(item => (
<li key={item.id} onClick={() => handleItemClick(item)}>
{item.title}
</li>
))}
</ul>
</div>
);
});
Best practices
Dependency arrays
Always include all values from component scope that are used inside the effect:
// ❌ Missing dependencies
lens.useEffect(() => {
fetchUserData(userId, apiKey); // userId and apiKey should be in deps
}, []);
// ✅ Complete dependencies
lens.useEffect(() => {
fetchUserData(userId, apiKey);
}, [userId, apiKey]);
Cleanup functions
Always clean up subscriptions, timers, and event listeners:
// ✅ Good: Proper cleanup
lens.useEffect(() => {
const subscription = eventEmitter.subscribe('event', handler);
const timer = setInterval(updateStats, 1000);
return () => {
subscription.unsubscribe();
clearInterval(timer);
};
}, []);
Avoid infinite loops
Be careful with object and array dependencies:
// ❌ This will cause infinite renders
const config = { endpoint: '/api/data', params: { limit: 10 } };
lens.useEffect(() => {
fetchData(config); // config is recreated every render
}, [config]);
// ✅ Better: Use primitive values or useMemo
const endpoint = '/api/data';
const limit = 10;
lens.useEffect(() => {
fetchData({ endpoint, params: { limit } });
}, [endpoint, limit]);
// ✅ Or memoize the config object
const config = lens.useMemo(() => ({
endpoint: '/api/data',
params: { limit: 10 }
}), []);
lens.useEffect(() => {
fetchData(config);
}, [config]);
Performance optimization
Use useMemo and useCallback wisely:
// ✅ Good: Expensive calculation
const expensiveValue = lens.useMemo(() => {
return largeDataSet.reduce((acc, item) => {
// Complex calculation
return acc + processItem(item);
}, 0);
}, [largeDataSet]);
// ❌ Don't memoize simple calculations
const simpleValue = lens.useMemo(() => {
return a + b; // This is not expensive enough to memoize
}, [a, b]);
// ✅ Good: Callback passed to child components
const handleClick = lens.useCallback((id) => {
doSomething(id);
}, [doSomething]);
// ❌ Don't memoize callbacks that aren't passed as props
const localHandler = lens.useCallback(() => {
console.log('clicked'); // Only used locally, no need to memoize
}, []);
TypeScript support
Type your lifecycle methods for better development experience:
import { ComponentFunction, Lens } from 'refract';
interface Props {
userId: string;
onUserLoad?: (user: User) => void;
}
interface User {
id: string;
name: string;
email: string;
}
const UserComponent: ComponentFunction<Props> = ({ lens, userId, onUserLoad }) => {
const user = lens.useRefraction<User | null>(null);
const loading = lens.useRefraction<boolean>(false);
// TypeScript knows the types of all variables
const fetchUser = lens.useCallback(async (id: string): Promise<void> => {
loading.set(true);
try {
const response = await fetch(`/api/users/${id}`);
const userData: User = await response.json();
user.set(userData);
onUserLoad?.(userData);
} catch (error) {
console.error('Failed to fetch user:', error);
} finally {
loading.set(false);
}
}, [onUserLoad]);
lens.useEffect(() => {
fetchUser(userId);
}, [userId, fetchUser]);
const userSummary = lens.useMemo((): string => {
return user.value ? `${user.value.name} (${user.value.email})` : 'No user';
}, [user.value]);
return (
<div>
{loading.value ? <p>Loading...</p> : <p>{userSummary}</p>}
</div>
);
};
Debugging lifecycle methods
Effect debugging
Add logging to understand when effects run:
const DebugComponent = createComponent(({ lens, prop1, prop2 }) => {
lens.useEffect(() => {
console.log('Effect 1 ran:', { prop1, prop2 });
return () => {
console.log('Effect 1 cleanup:', { prop1, prop2 });
};
}, [prop1, prop2]);
lens.useEffect(() => {
console.log('Effect 2 ran - mount only');
return () => {
console.log('Effect 2 cleanup - unmount only');
};
}, []); // Empty deps = mount/unmount only
return <div>Check console for effect logs</div>;
});
Memoization debugging
Check if your memoization is working:
const MemoDebugComponent = createComponent(({ lens, data }) => {
const expensiveResult = lens.useMemo(() => {
console.log('Expensive calculation running...'); // Should only log when data changes
return data.map(item => item.value * 2).reduce((a, b) => a + b, 0);
}, [data]);
const callback = lens.useCallback(() => {
console.log('Callback created'); // Should only log when dependencies change
}, []);
return <div>Result: {expensiveResult}</div>;
});
Understanding lifecycle methods is crucial for building efficient Refract applications. They give you precise control over when side effects run, help optimize performance, and ensure proper cleanup. Master these patterns and you'll be able to handle any complex UI scenario with confidence.