Skip to main content

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.0

Runs side effects with dependency tracking. Effects run after render and can return cleanup functions.

Signature

lens.useEffect(effect, dependencies)

Parameters

effect() => void | (() => void)required

Function to run. Can return a cleanup function.

dependenciesany[]required

Array 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 - Manual Dependencies
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

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>;
});

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>
);
});