Optics
Optics are Refract's secret weapon for creating reusable logic. Think of them as super-powered custom hooks that can encapsulate complex patterns - form handling, API calls, local storage, animations - and make them reusable across your entire application.
Instead of copying and pasting the same logic everywhere, you create an optic once and use it anywhere. It's like having a library of pre-built solutions for common problems.
Why Optics?
As your application grows, you'll find yourself writing the same patterns over and over - fetching data from APIs, managing form state, handling local storage. Instead of duplicating this logic, optics let you package it up into reusable functions.
The magic of optics is that they're automatically cached and optimized. When you use the same optic with the same parameters in multiple components, Refract ensures it only runs once and shares the result.
What Makes Optics Special?
-
Reusable Logic: Encapsulate complex patterns into reusable functions that can be shared across components.
-
Automatic Caching: Optics with the same parameters are automatically cached and shared between components.
-
Composable Patterns: Combine multiple optics together to create sophisticated behaviors from simple building blocks.
-
Built-in Optimization: Refract handles all the performance optimizations automatically.
Step-by-Step: Creating Your First Optic
An optic is simply a function that uses Refract's reactive features and returns an interface. Let's build one from scratch.
Step 1: Create the Optic Function
// Step 1: Create a function that encapsulates your logic
function useCounter(initialValue = 0) {
// Step 2: Use reactive features inside the optic
const count = lens.useRefraction(initialValue);
// ↑ This creates reactive state inside the optic
// Step 3: Create functions that work with the state
const increment = () => count.set(count.value + 1);
const decrement = () => count.set(count.value - 1);
const reset = () => count.set(initialValue);
// Step 4: Return an interface for components to use
return {
count: count.value, // ← Current count value
increment, // ← Function to increment
decrement, // ← Function to decrement
reset // ← Function to reset
};
}
Step 2: Use the Optic in Components
const CounterComponent = createComponent(({ lens }) => {
// Step 5: Use the optic in your component
const counter = lens.useOptic(() => useCounter(10));
// ↑ This calls your optic function and caches the result
return (
<div>
{/* Step 6: Use the optic's interface */}
<h2>Count: {counter.count}</h2>
<button onClick={counter.increment}>+</button>
<button onClick={counter.decrement}>-</button>
<button onClick={counter.reset}>Reset</button>
</div>
);
});
What Happens Here?
useCounterfunction - Encapsulates all counter logic in one place- Reactive state inside - The optic creates its own reactive state
- Clean interface - Returns only what components need to use
lens.useOptic()- Connects the optic to the component and handles caching- Reusable everywhere - Any component can now use this counter logic />
Best Practices
1. Keep Optics Focused
Each optic should have a single, clear responsibility:
Focused Optics
// ✅ Good - focused on one concern
function useCounter(initialValue = 0) {
const count = useRefraction(initialValue);
return {
count: count.value,
increment: () => count.set(count.value + 1),
decrement: () => count.set(count.value - 1),
reset: () => count.set(initialValue)
};
}
// ✅ Good - focused on API data
function useApiData(url) {
// ... API logic only
}
// ❌ Avoid - mixing concerns
function useCounterWithApi(url, initialCount) {
// Don't mix counter logic with API logic
// Better to compose separate optics
}
2. Return Consistent Interfaces
Design optic interfaces that are intuitive and consistent:
Consistent Interfaces
// ✅ Good - consistent naming and structure
function useApiData(url) {
return {
data: data.value,
loading: loading.value,
error: error.value,
refetch: fetchData
};
}
function useForm(initialValues, rules) {
return {
values: values.value,
errors: errors.value,
isValid: isValid.value,
handleSubmit: handleSubmit,
reset: reset
};
}
// ✅ Good - boolean states are clear
function useToggle(initialValue = false) {
return {
value: value.value,
toggle: () => value.set(!value.value),
setTrue: () => value.set(true),
setFalse: () => value.set(false)
};
}
Related resources
- Explore the useOptic API reference
- Learn about Refractions and Lenses
- Check out practical tutorials using optics