Skip to main content

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​

  1. useEffect: Handle lifecycle events, subscriptions, and reactive side effects with automatic cleanup.

  2. useOptic: Create reusable side effect patterns with dependency tracking and caching.

  3. 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​

Step-by-Step Timer Example
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:

  1. Effect runs when isActive changes - specified in dependencies array
  2. Early return for inactive state - no side effect needed when stopped
  3. Side effect setup - setInterval starts the timer
  4. Cleanup function - clearInterval stops the timer when effect re-runs
  5. Automatic cleanup - Refract calls cleanup when component unmounts

Key useEffect Patterns​

Common 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:

Simple Data Fetching
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>
);
});

useOptic - Reusable Side Effects​

Create reusable side effect patterns with optics:

Custom Optic for Local Storage
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:

Animation with useFlash
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:

Window Event Listeners
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:

Proper Cleanup
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