Skip to main content

Effects

Effects in Refract handle side effects and lifecycle management within components. They provide a clean, declarative way to manage asynchronous operations, subscriptions, timers, and other side effects while ensuring proper cleanup and dependency tracking.

Understanding Effects

Effects are functions that run in response to component lifecycle events or dependency changes. They're similar to React's useEffect but designed specifically for Refract's reactive system.

import { createComponent } from 'refract';

const EffectExample = createComponent(({ lens }) => {
const data = lens.useRefraction(null);

// Effect runs after component mounts
lens.useEffect(() => {
console.log('Component mounted');

// Cleanup function (optional)
return () => {
console.log('Component unmounting');
};
}, []); // Empty dependency array = run once on mount

// Effect runs when data changes
lens.useEffect(() => {
if (data.value) {
console.log('Data updated:', data.value);
}
}, [data.value]); // Runs when data.value changes

return <div>Effect Example</div>;
});

Effect Types

Mount Effects

Run once when the component mounts:

const MountEffect = createComponent(({ lens }) => {
const user = lens.useRefraction(null);

lens.useEffect(() => {
// Runs once on mount
fetchUserProfile().then(user.set);

// Optional cleanup
return () => {
console.log('Cleaning up user data');
};
}, []); // Empty dependency array

return <div>{user.value?.name || 'Loading...'}</div>;
});

Update Effects

Run when specific dependencies change:

const UpdateEffect = createComponent(({ lens, userId }) => {
const user = lens.useRefraction(null);
const loading = lens.useRefraction(false);

lens.useEffect(() => {
loading.set(true);

fetchUser(userId)
.then(user.set)
.finally(() => loading.set(false));
}, [userId]); // Runs when userId prop changes

return (
<div>
{loading.value ? 'Loading...' : user.value?.name}
</div>
);
});

Cleanup Effects

Handle resource cleanup when components unmount or dependencies change:

const CleanupEffect = createComponent(({ lens }) => {
const messages = lens.useRefraction([]);

lens.useEffect(() => {
// Set up subscription
const subscription = messageService.subscribe((message) => {
messages.set(prev => [...prev, message]);
});

// Cleanup subscription
return () => {
subscription.unsubscribe();
};
}, []);

return (
<div>
{messages.value.map(msg => (
<div key={msg.id}>{msg.text}</div>
))}
</div>
);
});

Advanced Effect Patterns

Async Effects

Handle asynchronous operations safely:

const AsyncEffect = createComponent(({ lens, searchQuery }) => {
const results = lens.useRefraction([]);
const loading = lens.useRefraction(false);
const error = lens.useRefraction(null);

lens.useEffect(() => {
if (!searchQuery) {
results.set([]);
return;
}

loading.set(true);
error.set(null);

// Use AbortController for cleanup
const abortController = new AbortController();

const performSearch = async () => {
try {
const response = await fetch(`/api/search?q=${searchQuery}`, {
signal: abortController.signal
});

if (!response.ok) {
throw new Error(`Search failed: ${response.status}`);
}

const data = await response.json();

// Only update if not aborted
if (!abortController.signal.aborted) {
results.set(data.results);
}
} catch (err) {
if (err.name !== 'AbortError') {
error.set(err.message);
}
} finally {
if (!abortController.signal.aborted) {
loading.set(false);
}
}
};

performSearch();

// Cleanup: abort the request
return () => {
abortController.abort();
};
}, [searchQuery]);

return (
<div>
{loading.value && <div>Searching...</div>}
{error.value && <div>Error: {error.value}</div>}
{results.value.map(result => (
<div key={result.id}>{result.title}</div>
))}
</div>
);
});

Debounced Effects

Delay effect execution until dependencies stabilize:

const DebouncedEffect = createComponent(({ lens }) => {
const searchTerm = lens.useRefraction('');
const debouncedTerm = lens.useRefraction('');
const results = lens.useRefraction([]);

// Debounce the search term
lens.useEffect(() => {
const timer = setTimeout(() => {
debouncedTerm.set(searchTerm.value);
}, 300);

return () => clearTimeout(timer);
}, [searchTerm.value]);

// Perform search when debounced term changes
lens.useEffect(() => {
if (debouncedTerm.value) {
performSearch(debouncedTerm.value).then(results.set);
} else {
results.set([]);
}
}, [debouncedTerm.value]);

return (
<div>
<input
value={searchTerm.value}
onChange={(e) => searchTerm.set(e.target.value)}
placeholder="Search..."
/>
<div>
{results.value.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
</div>
);
});

Interval Effects

Handle recurring operations:

const IntervalEffect = createComponent(({ lens }) => {
const time = lens.useRefraction(new Date());
const isActive = lens.useRefraction(true);

lens.useEffect(() => {
if (!isActive.value) return;

const interval = setInterval(() => {
time.set(new Date());
}, 1000);

return () => clearInterval(interval);
}, [isActive.value]);

return (
<div>
<p>Current time: {time.value.toLocaleTimeString()}</p>
<button onClick={() => isActive.set(!isActive.value)}>
{isActive.value ? 'Stop' : 'Start'} Clock
</button>
</div>
);
});

Event Listener Effects

Manage DOM event listeners:

const EventListenerEffect = createComponent(({ lens }) => {
const mousePosition = lens.useRefraction({ x: 0, y: 0 });
const isTracking = lens.useRefraction(false);

lens.useEffect(() => {
if (!isTracking.value) return;

const handleMouseMove = (event) => {
mousePosition.set({
x: event.clientX,
y: event.clientY
});
};

document.addEventListener('mousemove', handleMouseMove);

return () => {
document.removeEventListener('mousemove', handleMouseMove);
};
}, [isTracking.value]);

return (
<div>
<button onClick={() => isTracking.set(!isTracking.value)}>
{isTracking.value ? 'Stop' : 'Start'} Tracking
</button>
{isTracking.value && (
<p>Mouse: ({mousePosition.value.x}, {mousePosition.value.y})</p>
)}
</div>
);
});

Effect Optimization

Conditional Effects

Only run effects when necessary:

const ConditionalEffect = createComponent(({ lens, isEnabled, data }) => {
const processedData = lens.useRefraction(null);

lens.useEffect(() => {
// Only process data when enabled and data exists
if (isEnabled && data) {
const processed = expensiveProcessing(data);
processedData.set(processed);
} else {
processedData.set(null);
}
}, [isEnabled, data]); // Effect only runs when these change

return (
<div>
{processedData.value ? (
<DataDisplay data={processedData.value} />
) : (
<div>No processed data</div>
)}
</div>
);
});

Effect Dependencies

Properly manage effect dependencies to avoid unnecessary runs:

const DependencyExample = createComponent(({ lens, config }) => {
const data = lens.useRefraction(null);

// ✅ Good - Specific dependencies
lens.useEffect(() => {
fetchData(config.url, config.params).then(data.set);
}, [config.url, config.params]);

// ❌ Bad - Entire object as dependency
// lens.useEffect(() => {
// fetchData(config.url, config.params).then(data.set);
// }, [config]); // This runs whenever ANY config property changes

return <div>{data.value?.title}</div>;
});

Memoized Effects

Use memoization to prevent unnecessary effect runs:

const MemoizedEffect = createComponent(({ lens, items }) => {
const processedItems = lens.useRefraction([]);

// Memoize expensive computation
const itemIds = lens.useDerived(() =>
items.map(item => item.id).join(','),
[items]
);

lens.useEffect(() => {
// Only runs when item IDs change, not when other item properties change
const processed = items.map(processItem);
processedItems.set(processed);
}, [itemIds.value]);

return (
<div>
{processedItems.value.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
});

Flash Effects

Flash effects run once after the component renders, useful for animations and DOM manipulations:

const FlashEffect = createComponent(({ lens }) => {
const elementRef = lens.useRefraction(null);
const isVisible = lens.useRefraction(false);

// Flash effect runs after render
lens.useFlash(() => {
if (elementRef.value && isVisible.value) {
// Animate element entrance
elementRef.value.style.opacity = '0';
elementRef.value.style.transform = 'translateY(20px)';

requestAnimationFrame(() => {
elementRef.value.style.transition = 'all 0.3s ease';
elementRef.value.style.opacity = '1';
elementRef.value.style.transform = 'translateY(0)';
});
}
}, [isVisible.value]);

return (
<div>
<button onClick={() => isVisible.set(!isVisible.value)}>
Toggle Element
</button>
{isVisible.value && (
<div ref={(el) => elementRef.set(el)}>
Animated Element
</div>
)}
</div>
);
});

Error Handling in Effects

Try-Catch in Effects

const ErrorHandlingEffect = createComponent(({ lens }) => {
const data = lens.useRefraction(null);
const error = lens.useRefraction(null);

lens.useEffect(() => {
const loadData = async () => {
try {
error.set(null);
const result = await riskyAsyncOperation();
data.set(result);
} catch (err) {
console.error('Effect error:', err);
error.set(err.message);
}
};

loadData();
}, []);

if (error.value) {
return <div>Error: {error.value}</div>;
}

return <div>{data.value || 'Loading...'}</div>;
});

Error Boundaries for Effects

const withEffectErrorBoundary = (Component) => {
return createComponent((props) => {
const { lens } = props;
const hasError = lens.useRefraction(false);
const errorMessage = lens.useRefraction('');

// Wrap the original component with error handling
try {
if (hasError.value) {
return (
<div className="error-boundary">
<h2>Something went wrong</h2>
<p>{errorMessage.value}</p>
<button onClick={() => hasError.set(false)}>
Try Again
</button>
</div>
);
}

return <Component {...props} />;
} catch (error) {
hasError.set(true);
errorMessage.set(error.message);
return null;
}
});
};

Testing Effects

Mocking Effects

// Component with effects
const TimerComponent = createComponent(({ lens }) => {
const count = lens.useRefraction(0);

lens.useEffect(() => {
const interval = setInterval(() => {
count.set(prev => prev + 1);
}, 1000);

return () => clearInterval(interval);
}, []);

return <div>Count: {count.value}</div>;
});

// Test
import { render, act } from '@refract/testing-utils';

describe('TimerComponent', () => {
beforeEach(() => {
jest.useFakeTimers();
});

afterEach(() => {
jest.useRealTimers();
});

test('increments count every second', () => {
const { getByText } = render(<TimerComponent />);

expect(getByText('Count: 0')).toBeInTheDocument();

act(() => {
jest.advanceTimersByTime(1000);
});

expect(getByText('Count: 1')).toBeInTheDocument();

act(() => {
jest.advanceTimersByTime(2000);
});

expect(getByText('Count: 3')).toBeInTheDocument();
});
});

Best Practices

1. Always Handle Cleanup

// ✅ Good
lens.useEffect(() => {
const subscription = api.subscribe(handleData);
return () => subscription.unsubscribe();
}, []);

// ❌ Bad - No cleanup
lens.useEffect(() => {
api.subscribe(handleData);
}, []);

2. Use Specific Dependencies

// ✅ Good
lens.useEffect(() => {
fetchUser(userId);
}, [userId]);

// ❌ Bad - Missing dependencies
lens.useEffect(() => {
fetchUser(userId);
}, []);

3. Handle Async Operations Safely

// ✅ Good
lens.useEffect(() => {
let cancelled = false;

fetchData().then(data => {
if (!cancelled) {
setData(data);
}
});

return () => {
cancelled = true;
};
}, []);

// ❌ Bad - Race conditions possible
lens.useEffect(() => {
fetchData().then(setData);
}, []);

4. Separate Concerns

// ✅ Good - Separate effects for different concerns
lens.useEffect(() => {
// Handle user data
fetchUser(userId).then(setUser);
}, [userId]);

lens.useEffect(() => {
// Handle analytics
trackPageView(pageName);
}, [pageName]);

// ❌ Bad - Mixed concerns
lens.useEffect(() => {
fetchUser(userId).then(setUser);
trackPageView(pageName);
}, [userId, pageName]);

Next Steps

Now that you understand effects, explore: