useOptic
useOptic lets you create reusable pieces of logic that can be shared across multiple components. Think of it as a way to package up common patterns - like form handling, API calls, or local storage - into reusable functions.
It's like creating your own custom hooks, but with Refract's reactive system built in. Once you create an optic, you can use it anywhere in your application, and it will automatically handle state management, caching, and cleanup.
Why useOptic?
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 copying and pasting this logic, useOptic lets you create reusable patterns that work seamlessly with Refract's reactive system.
The key benefit is that optics are automatically cached based on their dependencies, so expensive operations only run when they need to.
lens.useOptic
Creates a reusable logic pattern with dependency tracking and caching.
Signature
const result = lens.useOptic(opticFunction, dependencies?)
Parameters
opticFunction(lens) => anyrequiredFunction that defines the optic logic using the provided lens
dependenciesany[]Optional array of dependencies for caching
Returns
The result returned by the optic function
Example
// Create a reusable form optic
const formData = lens.useOptic((lens) => {
const values = lens.useRefraction({});
const errors = lens.useRefraction({});
return { values, errors, isValid: Object.keys(errors.value).length === 0 };
});
Comparison with Traditional Approaches
Let's see how useOptic compares to traditional ways of sharing logic:
- React Custom Hook
- Refract useOptic
- Live Result
import { useState, useEffect } from 'react';
// Custom hook for API calls
const useApi = (url) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
setError(null);
fetch(url)
.then(response => response.json())
.then(result => {
setData(result);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, [url]); // Must remember dependencies
const refetch = () => {
// Duplicate logic for refetching
setLoading(true);
setError(null);
fetch(url)
.then(response => response.json())
.then(result => {
setData(result);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
};
return { data, loading, error, refetch };
};
// Usage in component
const UserList = () => {
const { data: users, loading, error, refetch } = useApi('/api/users');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
{users?.map(user => <div key={user.id}>{user.name}</div>)}
<button onClick={refetch}>Refresh</button>
</div>
);
};
Issues with React custom hooks:
- Duplicate logic for similar operations (refetch)
- Manual dependency management
- No automatic caching
- Verbose for simple patterns
// Reusable API optic
const useApi = (url) => {
return lens.useOptic((lens) => {
const data = lens.useRefraction(null);
const loading = lens.useRefraction(true);
const error = lens.useRefraction(null);
const fetchData = async () => {
loading.set(true);
error.set(null);
try {
const response = await fetch(url);
const result = await response.json();
data.set(result);
} catch (err) {
error.set(err.message);
} finally {
loading.set(false);
}
};
// Auto-fetch on mount
lens.useEffect(() => {
fetchData();
}, [url]);
return {
data: data.value,
loading: loading.value,
error: error.value,
refetch: fetchData // Reuse the same logic
};
}, [url]); // Automatic caching based on URL
};
// Usage in component
const UserList = createComponent(({ lens }) => {
const { data: users, loading, error, refetch } = useApi('/api/users');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
{users?.map(user => <div key={user.id}>{user.name}</div>)}
<button onClick={refetch}>Refresh</button>
</div>
);
});
Benefits of Refract useOptic:
- Automatic caching based on dependencies
- Reusable logic without duplication
- Integrated with reactive system
- Cleaner, more maintainable code
Optic automatically caches results and handles loading states
Basic Usage
Create reusable logic that can be shared between components:
const useCounter = (initialValue = 0) => {
return lens.useOptic((lens) => {
const count = lens.useRefraction(initialValue);
const increment = () => count.set(count.value + 1);
const decrement = () => count.set(count.value - 1);
const reset = () => count.set(initialValue);
return {
count: count.value,
increment,
decrement,
reset
};
});
};
const Counter = createComponent(({ lens }) => {
const counter = useCounter(0);
return (
<div>
<h2>Count: {counter.count}</h2>
<button onClick={counter.increment}>+</button>
<button onClick={counter.decrement}>-</button>
<button onClick={counter.reset}>Reset</button>
</div>
);
});
Form Management Optic
Build complex form logic that can be reused across your app:
- Form Optic
- Using the Form
const useForm = (initialValues = {}, validationRules = {}) => {
return lens.useOptic((lens) => {
const values = lens.useRefraction(initialValues);
const errors = lens.useRefraction({});
const touched = lens.useRefraction({});
const isSubmitting = lens.useRefraction(false);
const validate = (fieldName, value) => {
const rule = validationRules[fieldName];
if (!rule) return null;
if (rule.required && (!value || value.trim() === '')) {
return 'This field is required';
}
if (rule.minLength && value.length < rule.minLength) {
return `Must be at least ${rule.minLength} characters`;
}
if (rule.pattern && !rule.pattern.test(value)) {
return rule.message || 'Invalid format';
}
return null;
};
const setValue = (fieldName, value) => {
values.set({ ...values.value, [fieldName]: value });
// Validate if field has been touched
if (touched.value[fieldName]) {
const error = validate(fieldName, value);
errors.set({
...errors.value,
[fieldName]: error
});
}
};
const setTouched = (fieldName) => {
touched.set({ ...touched.value, [fieldName]: true });
// Validate when field is touched
const value = values.value[fieldName];
const error = validate(fieldName, value);
errors.set({
...errors.value,
[fieldName]: error
});
};
const validateAll = () => {
const newErrors = {};
Object.keys(validationRules).forEach(fieldName => {
const error = validate(fieldName, values.value[fieldName]);
if (error) newErrors[fieldName] = error;
});
errors.set(newErrors);
return Object.keys(newErrors).length === 0;
};
const reset = () => {
values.set(initialValues);
errors.set({});
touched.set({});
isSubmitting.set(false);
};
return {
values: values.value,
errors: errors.value,
touched: touched.value,
isSubmitting: isSubmitting.value,
isValid: Object.keys(errors.value).length === 0,
setValue,
setTouched,
validateAll,
reset,
setSubmitting: (value) => isSubmitting.set(value)
};
});
};
const ContactForm = createComponent(({ lens }) => {
const form = useForm(
{ name: '', email: '', message: '' },
{
name: { required: true, minLength: 2 },
email: {
required: true,
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: 'Please enter a valid email'
},
message: { required: true, minLength: 10 }
}
);
const handleSubmit = async (e) => {
e.preventDefault();
if (!form.validateAll()) return;
form.setSubmitting(true);
try {
await submitContactForm(form.values);
form.reset();
alert('Message sent successfully!');
} catch (error) {
alert('Failed to send message');
} finally {
form.setSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<input
type="text"
placeholder="Name"
value={form.values.name}
onChange={(e) => form.setValue('name', e.target.value)}
onBlur={() => form.setTouched('name')}
/>
{form.errors.name && <span className="error">{form.errors.name}</span>}
</div>
<div>
<input
type="email"
placeholder="Email"
value={form.values.email}
onChange={(e) => form.setValue('email', e.target.value)}
onBlur={() => form.setTouched('email')}
/>
{form.errors.email && <span className="error">{form.errors.email}</span>}
</div>
<div>
<textarea
placeholder="Message"
value={form.values.message}
onChange={(e) => form.setValue('message', e.target.value)}
onBlur={() => form.setTouched('message')}
/>
{form.errors.message && <span className="error">{form.errors.message}</span>}
</div>
<button
type="submit"
disabled={!form.isValid || form.isSubmitting}
>
{form.isSubmitting ? 'Sending...' : 'Send Message'}
</button>
</form>
);
});
Related APIs
- useRefraction - Managing reactive state
- useEffect - Side effects and lifecycle
- Optics concept - Understanding optical composition
- Components - Component patterns