useEffect
useEffect lets you perform side effects in your Refract components - things like API calls, timers, subscriptions, or DOM manipulation. It's your bridge between Refract's reactive world and the outside world.
Think of it as your component's way of saying "when something changes, I need to do something about it." Unlike React's useEffect, Refract's version is deeply integrated with the reactive system, making it more predictable and easier to use.
Why useEffect?
In any real application, you need to interact with things outside your component - fetch data from APIs, set up timers, listen to events, or update the document title. These are called "side effects" because they affect something outside the component itself.
Refract's useEffect makes these interactions clean and automatic. When your reactive state changes, your effects run automatically. When your component unmounts, cleanup happens automatically.
lens.useEffect
Since v1.0.0Runs side effects with dependency tracking. Effects run after render and can return cleanup functions.
Signature
lens.useEffect(effect, dependencies)
Parameters
effect() => void | (() => void)requiredFunction to run. Can return a cleanup function.
dependenciesany[]requiredArray of dependencies. Effect runs when dependencies change.
Example
// Run once on mount
lens.useEffect(() => {
console.log('Component mounted');
return () => console.log('Cleanup');
}, []);
// Run when state changes
lens.useEffect(() => {
document.title = `Count: ${count.value}`;
}, [count.value]);
Comparison with Traditional Approaches
Let's see how Refract's useEffect compares to other approaches for handling side effects:
- React useEffect
- Refract useEffect
- Live Result
import React, { useState, useEffect } from 'react';
const UserProfile = ({ userId }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
setError(null);
fetchUser(userId)
.then(userData => {
setUser(userData);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, [userId]); // Must remember to include userId in dependencies
// Easy to forget cleanup
useEffect(() => {
const timer = setInterval(() => {
console.log('Still mounted');
}, 1000);
return () => clearInterval(timer); // Must remember cleanup
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return <div>Welcome, {user?.name}!</div>;
};
Issues with React useEffect:
- Easy to forget dependencies (causes stale closures)
- Manual state management for loading/error states
- Must remember cleanup functions
- Dependencies array can become complex
import { createComponent } from 'refract';
const UserProfile = createComponent(({ lens, userId }) => {
const user = lens.useRefraction(null);
const loading = lens.useRefraction(true);
const error = lens.useRefraction(null);
lens.useEffect(() => {
loading.set(true);
error.set(null);
fetchUser(userId)
.then(userData => {
user.set(userData);
loading.set(false);
})
.catch(err => {
error.set(err.message);
loading.set(false);
});
}, [userId]); // Clear, simple dependencies
// Automatic cleanup when component unmounts
lens.useEffect(() => {
const timer = setInterval(() => {
console.log('Still mounted');
}, 1000);
return () => clearInterval(timer); // Cleanup handled automatically
}, []);
if (loading.value) return <div>Loading...</div>;
if (error.value) return <div>Error: {error.value}</div>;
return <div>Welcome, {user.value?.name}!</div>;
});
Benefits of Refract useEffect:
- Reactive state updates automatically trigger re-renders
- Clear dependency tracking
- Automatic cleanup when component unmounts
- Integrated with Refract's reactive system
Effect runs automatically when userId changes, with proper cleanup
Basic Usage
Mount and Cleanup Effects
Running effects on component mount with cleanup:
import { createComponent } from "refract";
const TimerComponent = createComponent(({ lens }) => {
const seconds = lens.useRefraction(0);
const isRunning = lens.useRefraction(false);
// Effect runs once on mount
lens.useEffect(() => {
console.log('Timer component mounted');
// Cleanup function runs when component unmounts
return () => {
console.log('Timer component unmounting');
};
}, []); // Empty dependency array = run once
// Effect for timer logic
lens.useEffect(() => {
let interval;
if (isRunning.value) {
interval = setInterval(() => {
seconds.set(prev => prev + 1);
}, 1000);
}
// Cleanup function clears the interval
return () => {
if (interval) {
clearInterval(interval);
}
};
}, [isRunning.value]); // Runs when isRunning changes
return (
<div>
<h2>Timer: {seconds.value}s</h2>
<button onClick={() => isRunning.set(!isRunning.value)}>
{isRunning.value ? 'Stop' : 'Start'}
</button>
<button onClick={() => seconds.set(0)}>
Reset
</button>
</div>
);
});
Data Fetching
Data Fetching with Effects
Using effects for API calls and data management:
const UserProfile = createComponent(({ lens, userId }) => {
const user = lens.useRefraction(null);
const loading = lens.useRefraction(true);
const error = lens.useRefraction(null);
// Effect runs when userId changes
lens.useEffect(() => {
// Reset state when starting new fetch
loading.set(true);
error.set(null);
user.set(null);
// Create AbortController for cleanup
const abortController = new AbortController();
const fetchUser = async () => {
try {
const response = await fetch(`/api/users/${userId}`, {
signal: abortController.signal
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const userData = await response.json();
user.set(userData);
loading.set(false);
} catch (err) {
if (err.name !== 'AbortError') {
error.set(err.message);
loading.set(false);
}
}
};
fetchUser();
// Cleanup function cancels the request
return () => {
abortController.abort();
};
}, [userId]); // Runs when userId changes
// Effect for updating document title
lens.useEffect(() => {
if (user.value) {
document.title = `${user.value.name} - Profile`;
}
return () => {
document.title = 'My App'; // Reset title on cleanup
};
}, [user.value?.name]);
if (loading.value) return <div>Loading user...</div>;
if (error.value) return <div>Error: {error.value}</div>;
if (!user.value) return <div>User not found</div>;
return (
<div className="user-profile">
<img src={user.value.avatar} alt={user.value.name} />
<h1>{user.value.name}</h1>
<p>{user.value.email}</p>
<p>Joined: {new Date(user.value.createdAt).toLocaleDateString()}</p>
</div>
);
});
Event Listeners
Managing Event Listeners
Adding and cleaning up event listeners:
const KeyboardHandler = createComponent(({ lens }) => {
const pressedKeys = lens.useRefraction(new Set());
const lastKey = lens.useRefraction("");
// Effect for keyboard event listeners
lens.useEffect(() => {
const handleKeyDown = (event) => {
pressedKeys.set(new Set([...pressedKeys.value, event.key]));
lastKey.set(event.key);
};
const handleKeyUp = (event) => {
const newKeys = new Set(pressedKeys.value);
newKeys.delete(event.key);
pressedKeys.set(newKeys);
};
// Add event listeners
document.addEventListener('keydown', handleKeyDown);
document.addEventListener('keyup', handleKeyUp);
// Cleanup function removes event listeners
return () => {
document.removeEventListener('keydown', handleKeyDown);
document.removeEventListener('keyup', handleKeyUp);
};
}, []); // Empty dependency array = setup once
return (
<div>
<h2>Keyboard Monitor</h2>
<p>Last key pressed: {lastKey.value}</p>
<p>Currently pressed keys: {Array.from(pressedKeys.value).join(', ')}</p>
<p>Press any keys to see them here!</p>
</div>
);
});
Local Storage Sync
Syncing with Local Storage
Using effects to persist state to localStorage:
const Settings = createComponent(({ lens }) => {
const theme = lens.useRefraction("light");
const language = lens.useRefraction("en");
const notifications = lens.useRefraction(true);
// Load settings from localStorage on mount
lens.useEffect(() => {
const savedSettings = localStorage.getItem('app-settings');
if (savedSettings) {
try {
const settings = JSON.parse(savedSettings);
theme.set(settings.theme || "light");
language.set(settings.language || "en");
notifications.set(settings.notifications ?? true);
} catch (error) {
console.warn('Failed to load settings from localStorage:', error);
}
}
}, []); // Run once on mount
// Save settings to localStorage when they change
lens.useEffect(() => {
const settings = {
theme: theme.value,
language: language.value,
notifications: notifications.value
};
try {
localStorage.setItem('app-settings', JSON.stringify(settings));
} catch (error) {
console.warn('Failed to save settings to localStorage:', error);
}
}, [theme.value, language.value, notifications.value]);
return (
<div className="settings">
<h2>Settings</h2>
<div className="setting">
<label>Theme:</label>
<select value={theme.value} onChange={(e) => theme.set(e.target.value)}>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</div>
<div className="setting">
<label>Language:</label>
<select value={language.value} onChange={(e) => language.set(e.target.value)}>
<option value="en">English</option>
<option value="es">Spanish</option>
<option value="fr">French</option>
</select>
</div>
<div className="setting">
<label>
<input
type="checkbox"
checked={notifications.value}
onChange={(e) => notifications.set(e.target.checked)}
/>
Enable notifications
</label>
</div>
</div>
);
});
Common Patterns
Debounced Effects
Debounced Search
const DebouncedSearch = createComponent(({ lens }) => {
const query = lens.useRefraction("");
const results = lens.useRefraction([]);
// Debounced search effect
lens.useEffect(() => {
const timeoutId = setTimeout(() => {
if (query.value.trim()) {
searchAPI(query.value).then(results.set);
} else {
results.set([]);
}
}, 300); // 300ms debounce
return () => clearTimeout(timeoutId);
}, [query.value]);
return (
<div>
<input
value={query.value}
onChange={(e) => query.set(e.target.value)}
placeholder="Search (debounced)..."
/>
{results.value.map(result => (
<div key={result.id}>{result.title}</div>
))}
</div>
);
});
Conditional Effects
Conditional Effect Execution
const ConditionalEffect = createComponent(({ lens, shouldFetch, userId }) => {
const data = lens.useRefraction(null);
lens.useEffect(() => {
// Only run effect when condition is met
if (shouldFetch && userId) {
fetchUserData(userId).then(data.set);
}
}, [shouldFetch, userId]); // Include condition in dependencies
return <div>{data.value ? data.value.name : 'No data'}</div>;
});
Related APIs
- useRefraction - Managing reactive state
- useFlash - One-time effects after render
- Lenses concept - Understanding the lens object
WebSocket Connection
Managing WebSocket Connections
Using effects for WebSocket lifecycle management:
const ChatComponent = createComponent(({ lens, roomId }) => {
const messages = lens.useRefraction([]);
const connectionStatus = lens.useRefraction("disconnected");
const newMessage = lens.useRefraction("");
// Effect for WebSocket connection
lens.useEffect(() => {
connectionStatus.set("connecting");
const ws = new WebSocket(`ws://localhost:8080/chat/${roomId}`);
ws.onopen = () => {
connectionStatus.set("connected");
console.log('Connected to chat room:', roomId);
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
messages.set(prev => [...prev, message]);
};
ws.onclose = () => {
connectionStatus.set("disconnected");
console.log('Disconnected from chat room');
};
ws.onerror = (error) => {
connectionStatus.set("error");
console.error('WebSocket error:', error);
};
// Store ws reference for sending messages
const sendMessage = (text) => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'message',
text,
timestamp: Date.now()
}));
}
};
// Cleanup function closes the connection
return () => {
ws.close();
};
}, [roomId]); // Reconnect when roomId changes
const handleSendMessage = () => {
if (newMessage.value.trim()) {
// This would need access to the WebSocket instance
// In a real app, you'd store the ws reference in state
newMessage.set("");
}
};
return (
<div className="chat">
<div className="connection-status">
Status: {connectionStatus.value}
</div>
<div className="messages">
{messages.value.map((msg, index) => (
<div key={index} className="message">
<span className="timestamp">
{new Date(msg.timestamp).toLocaleTimeString()}
</span>
<span className="text">{msg.text}</span>
</div>
))}
</div>
<div className="message-input">
<input
type="text"
value={newMessage.value}
onChange={(e) => newMessage.set(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSendMessage()}
placeholder="Type a message..."
disabled={connectionStatus.value !== "connected"}
/>
<button
onClick={handleSendMessage}
disabled={connectionStatus.value !== "connected"}
>
Send
</button>
</div>
</div>
);
});