Skip to main content

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) => anyrequired

Function that defines the optic logic using the provided lens

dependenciesany[]

Optional array of dependencies for caching

Returns

any

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:

Traditional React Custom Hook
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

Basic Usage​

Create reusable logic that can be shared between components:

Simple Counter Optic
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:

Reusable Form Optic
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)
};
});
};