Side Effects
Side effects are the bridge between your reactive components and the outside world. They handle API calls, timers, DOM manipulation, subscriptions - anything that reaches beyond your component's pure rendering logic.
Refract provides three powerful tools for managing side effects, each designed for different scenarios. Think of them as your toolkit for safely interacting with the world outside your components.
Why Side Effects Matter
Pure components are predictable - given the same props, they always render the same output. But real applications need to fetch data, respond to user interactions, and communicate with external services. Side effects let you do this safely without breaking the predictable nature of your components.
The key is managing side effects in a controlled way so they don't cause bugs, memory leaks, or unpredictable behavior.
Refract's Side Effect Tools
-
useEffect: Handle lifecycle events, subscriptions, and reactive side effects with automatic cleanup.
-
useOptic: Create reusable side effect patterns with dependency tracking and caching.
-
useFlash: Run one-time effects after render, perfect for animations and DOM updates.
Each tool is designed for specific scenarios, and choosing the right one makes your code cleaner and more reliable.
useEffect - Reactive Side Effects
useEffect is your go-to tool for side effects that need to respond to changes in your reactive state. It's like React's useEffect, but designed to work seamlessly with Refract's reactive system.
Step-by-Step: Building a Timer with useEffect
const Timer = createComponent(({ lens }) => {
// Step 1: Create reactive state
const seconds = lens.useRefraction(0);
const isActive = lens.useRefraction(false);
// Step 2: Set up an effect that responds to state changes
lens.useEffect(() => {
// Step 3: Check if the timer should be running
if (!isActive.value) return;
// ↑ If not active, exit early (no cleanup needed)
// Step 4: Start the side effect (timer)
const interval = setInterval(() => {
seconds.set(s => s + 1); // Update reactive state
}, 1000);
// Step 5: Return cleanup function
return () => clearInterval(interval);
// ↑ This runs when the effect re-runs or component unmounts
}, [isActive.value]); // Step 6: Dependencies - effect runs when isActive changes
return (
<div>
<h2>{seconds.value} seconds</h2>
<button onClick={() => isActive.set(!isActive.value)}>
{isActive.value ? 'Stop' : 'Start'}
</button>
</div>
);
});
What happens here:
- Effect runs when
isActivechanges - specified in dependencies array - Early return for inactive state - no side effect needed when stopped
- Side effect setup -
setIntervalstarts the timer - Cleanup function -
clearIntervalstops the timer when effect re-runs - Automatic cleanup - Refract calls cleanup when component unmounts
Key useEffect Patterns
// Pattern 1: Run once on mount
lens.useEffect(() => {
console.log('Component mounted');
}, []); // Empty dependencies = run once
// Pattern 2: Run when specific state changes
lens.useEffect(() => {
console.log('User changed:', user.value);
}, [user.value]); // Runs when user changes
// Pattern 3: Cleanup subscriptions
lens.useEffect(() => {
const subscription = api.subscribe(data => {
setData(data);
});
return () => subscription.unsubscribe(); // Cleanup
}, []);
// Pattern 4: Conditional effects
lens.useEffect(() => {
if (!shouldRun.value) return; // Exit early
// Do side effect only when condition is met
performSideEffect();
}, [shouldRun.value]);
API Calls and Data Fetching
Handle async operations with proper loading and error states:
- Basic Fetch
- Advanced Pattern
const UserProfile = createComponent(({ lens, userId }) => {
const user = lens.useRefraction(null);
const loading = lens.useRefraction(true);
const error = lens.useRefraction(null);
lens.useEffect(() => {
const fetchUser = async () => {
loading.set(true);
error.set(null);
try {
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
user.set(userData);
} catch (err) {
error.set(err.message);
} finally {
loading.set(false);
}
};
fetchUser();
}, [userId]);
if (loading.value) return <div>Loading...</div>;
if (error.value) return <div>Error: {error.value}</div>;
if (!user.value) return <div>User not found</div>;
return (
<div>
<h1>{user.value.name}</h1>
<p>{user.value.email}</p>
</div>
);
});
const useApi = (lens, url, dependencies = []) => {
const data = lens.useRefraction(null);
const loading = lens.useRefraction(false);
const error = lens.useRefraction(null);
const refetch = () => {
loading.set(true);
error.set(null);
fetch(url)
.then(response => response.json())
.then(result => {
data.set(result);
loading.set(false);
})
.catch(err => {
error.set(err.message);
loading.set(false);
});
};
lens.useEffect(() => {
refetch();
}, dependencies);
return { data: data.value, loading: loading.value, error: error.value, refetch };
};
const PostList = createComponent(({ lens }) => {
const { data: posts, loading, error, refetch } = useApi(lens, '/api/posts');
if (loading) return <div>Loading posts...</div>;
if (error) return <div>Error: {error} <button onClick={refetch}>Retry</button></div>;
return (
<div>
{posts?.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
);
});
useOptic - Reusable Side Effects
Create reusable side effect patterns with optics:
const useLocalStorage = (key, defaultValue) => {
return lens.useOptic((lens) => {
const stored = lens.useRefraction(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
} catch {
return defaultValue;
}
});
// Save to localStorage when value changes
lens.useEffect(() => {
localStorage.setItem(key, JSON.stringify(stored.value));
}, [stored.value]);
return stored;
});
};
const Settings = createComponent(({ lens }) => {
const theme = useLocalStorage(lens, 'theme', 'light');
const language = useLocalStorage(lens, 'language', 'en');
return (
<div>
<select
value={theme.value}
onChange={(e) => theme.set(e.target.value)}
>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
<select
value={language.value}
onChange={(e) => language.set(e.target.value)}
>
<option value="en">English</option>
<option value="es">Spanish</option>
</select>
</div>
);
});
useFlash - One-time Effects
Perfect for animations and DOM manipulations that should happen after render:
const NotificationToast = createComponent(({ lens, message, onClose }) => {
const isVisible = lens.useRefraction(false);
// Animate in after mount
lens.useFlash(() => {
requestAnimationFrame(() => {
isVisible.set(true);
});
// Auto-close after 3 seconds
const timer = setTimeout(() => {
isVisible.set(false);
setTimeout(onClose, 300); // Wait for animation
}, 3000);
return () => clearTimeout(timer);
});
return (
<div
className={`toast ${isVisible.value ? 'toast-visible' : 'toast-hidden'}`}
onClick={() => isVisible.set(false)}
>
{message}
</div>
);
});
Event Listeners and Subscriptions
Handle global events and subscriptions:
const WindowSize = createComponent(({ lens }) => {
const windowSize = lens.useRefraction({
width: window.innerWidth,
height: window.innerHeight
});
lens.useEffect(() => {
const handleResize = () => {
windowSize.set({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []); // Empty dependencies - runs once
return (
<div>
Window size: {windowSize.value.width} x {windowSize.value.height}
</div>
);
});
Cleanup and Memory Management
Always clean up side effects to prevent memory leaks:
const WebSocketConnection = createComponent(({ lens, url }) => {
const messages = lens.useRefraction([]);
const connectionStatus = lens.useRefraction('disconnected');
lens.useEffect(() => {
const ws = new WebSocket(url);
ws.onopen = () => connectionStatus.set('connected');
ws.onclose = () => connectionStatus.set('disconnected');
ws.onerror = () => connectionStatus.set('error');
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
messages.set(prev => [...prev, message]);
};
// Cleanup: close WebSocket when effect re-runs or component unmounts
return () => {
ws.close();
};
}, [url]);
return (
<div>
<div>Status: {connectionStatus.value}</div>
<div>Messages: {messages.value.length}</div>
</div>
);
});
Best Practices
- Always provide cleanup: Return cleanup functions from useEffect
- Minimize dependencies: Only include values that actually affect the effect
- Handle loading states: Show appropriate UI during async operations
- Error boundaries: Always handle errors in async operations
- Avoid infinite loops: Be careful with effect dependencies
- Use useFlash for DOM: Perfect for animations and one-time DOM updates
Related resources
- useEffect - Reactive side effects
- useOptic - Reusable effect patterns
- Components - Component lifecycle
- Refractions - Reactive state management