useRefraction
Creates a reactive state value that automatically updates your UI when changed. Think of it as Refract's version of useState, but more powerful—it works with any data type and provides fine-grained reactivity.
Reference
const state = lens.useRefraction(initialValue);
Parameters
- initialValue: The starting value for your reactive state. Can be any type: primitives, objects, arrays, functions, etc.
Returns
A refraction object with these properties:
- value: The current value of the state
- set(newValue): Updates the state to a new value
- update(updateFn): Updates the state using a function that receives the current value
Usage
Basic state management
Use refractions for any value that should trigger UI updates when changed:
const Counter = createComponent(({ lens }) => {
const count = lens.useRefraction(0);
return (
<div>
<p>Count: {count.value}</p>
<button onClick={() => count.set(count.value + 1)}>
Increment
</button>
<button onClick={() => count.set(0)}>
Reset
</button>
</div>
);
});
Working with objects
Refractions work seamlessly with objects. Remember to create new objects when updating:
const UserForm = createComponent(({ lens }) => {
const user = lens.useRefraction({
name: '',
email: '',
age: 0
});
const updateName = (name) => {
user.update(current => ({ ...current, name }));
};
const updateEmail = (email) => {
user.update(current => ({ ...current, email }));
};
return (
<form>
<input
value={user.value.name}
onChange={(e) => updateName(e.target.value)}
placeholder="Name"
/>
<input
value={user.value.email}
onChange={(e) => updateEmail(e.target.value)}
placeholder="Email"
type="email"
/>
<p>User: {JSON.stringify(user.value, null, 2)}</p>
</form>
);
});
Working with arrays
Managing lists is straightforward with the update method:
const TodoList = createComponent(({ lens }) => {
const todos = lens.useRefraction([]);
const newTodo = lens.useRefraction('');
const addTodo = () => {
if (newTodo.value.trim()) {
todos.update(current => [
...current,
{ id: Date.now(), text: newTodo.value, completed: false }
]);
newTodo.set('');
}
};
const toggleTodo = (id) => {
todos.update(current =>
current.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
const removeTodo = (id) => {
todos.update(current => current.filter(todo => todo.id !== id));
};
return (
<div>
<div>
<input
value={newTodo.value}
onChange={(e) => newTodo.set(e.target.value)}
placeholder="Add a todo..."
onKeyDown={(e) => e.key === 'Enter' && addTodo()}
/>
<button onClick={addTodo}>Add</button>
</div>
<ul>
{todos.value.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span style={{
textDecoration: todo.completed ? 'line-through' : 'none'
}}>
{todo.text}
</span>
<button onClick={() => removeTodo(todo.id)}>Remove</button>
</li>
))}
</ul>
</div>
);
});
Complex state updates
For more complex state logic, you can use the update method with sophisticated update functions:
const ShoppingCart = createComponent(({ lens }) => {
const cart = lens.useRefraction({
items: [],
total: 0,
discount: 0
});
const addItem = (product) => {
cart.update(current => {
const existingItem = current.items.find(item => item.id === product.id);
let newItems;
if (existingItem) {
newItems = current.items.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
);
} else {
newItems = [...current.items, { ...product, quantity: 1 }];
}
const newTotal = newItems.reduce(
(sum, item) => sum + (item.price * item.quantity),
0
);
return {
...current,
items: newItems,
total: newTotal - current.discount
};
});
};
const applyDiscount = (amount) => {
cart.update(current => ({
...current,
discount: amount,
total: current.items.reduce(
(sum, item) => sum + (item.price * item.quantity),
0
) - amount
}));
};
return (
<div>
<h2>Shopping Cart</h2>
{cart.value.items.map(item => (
<div key={item.id}>
{item.name} x {item.quantity} = ${item.price * item.quantity}
</div>
))}
<p>Discount: -${cart.value.discount}</p>
<p><strong>Total: ${cart.value.total}</strong></p>
</div>
);
});
Derived state
You can create computed values that depend on refractions using useMemo:
const OrderSummary = createComponent(({ lens }) => {
const items = lens.useRefraction([
{ name: 'Laptop', price: 999, quantity: 1 },
{ name: 'Mouse', price: 25, quantity: 2 }
]);
const taxRate = lens.useRefraction(0.08);
// Derived state
const subtotal = lens.useMemo(() => {
return items.value.reduce(
(sum, item) => sum + (item.price * item.quantity),
0
);
}, [items.value]);
const tax = lens.useMemo(() => {
return subtotal * taxRate.value;
}, [subtotal, taxRate.value]);
const total = lens.useMemo(() => {
return subtotal + tax;
}, [subtotal, tax]);
return (
<div>
<h3>Order Summary</h3>
<p>Subtotal: ${subtotal.toFixed(2)}</p>
<p>Tax ({(taxRate.value * 100).toFixed(1)}%): ${tax.toFixed(2)}</p>
<p><strong>Total: ${total.toFixed(2)}</strong></p>
<div>
<label>
Tax rate:
<input
type="number"
step="0.01"
value={taxRate.value}
onChange={(e) => taxRate.set(parseFloat(e.target.value))}
/>
</label>
</div>
</div>
);
});
Global vs Local State
Local refractions
By default, refractions are local to the component where they're created:
const LocalCounter = createComponent(({ lens }) => {
const count = lens.useRefraction(0); // This is local to this component
return (
<button onClick={() => count.set(count.value + 1)}>
Local count: {count.value}
</button>
);
});
// Each instance has its own count
const App = createComponent(({ lens }) => (
<div>
<LocalCounter /> {/* Independent counter */}
<LocalCounter /> {/* Independent counter */}
</div>
));
Global refractions
For state that needs to be shared across components, use useGlobalRefraction:
import { useGlobalRefraction } from 'refract';
const GlobalCounter = createComponent(({ lens }) => {
const count = useGlobalRefraction('global-count', 0); // Shared across all instances
return (
<button onClick={() => count.set(count.value + 1)}>
Global count: {count.value}
</button>
);
});
// Both instances share the same count
const App = createComponent(({ lens }) => (
<div>
<GlobalCounter /> {/* Shared counter */}
<GlobalCounter /> {/* Same counter */}
</div>
));
TypeScript
When using TypeScript, you can specify the type of your refraction:
import { Refraction } from 'refract';
interface User {
id: number;
name: string;
email: string;
}
const UserProfile = createComponent(({ lens }) => {
// Type the refraction
const user: Refraction<User | null> = lens.useRefraction(null);
const loading: Refraction<boolean> = lens.useRefraction(false);
// TypeScript now knows the types
const loadUser = async (id: number) => {
loading.set(true);
try {
const response = await fetch(`/api/users/${id}`);
const userData: User = await response.json();
user.set(userData);
} catch (error) {
console.error('Failed to load user:', error);
} finally {
loading.set(false);
}
};
return (
<div>
{loading.value ? (
<p>Loading...</p>
) : user.value ? (
<div>
<h2>{user.value.name}</h2>
<p>{user.value.email}</p>
</div>
) : (
<p>No user loaded</p>
)}
</div>
);
});
Performance tips
Avoid creating new objects in render
Don't create new objects during render—it can cause unnecessary updates:
// ❌ Don't do this
const BadExample = createComponent(({ lens }) => {
const user = lens.useRefraction({ name: 'John', age: 30 }); // New object every render!
return <div>{user.value.name}</div>;
});
// ✅ Do this instead
const GoodExample = createComponent(({ lens }) => {
const user = lens.useRefraction(() => ({ name: 'John', age: 30 })); // Created once
return <div>{user.value.name}</div>;
});
Use update for complex changes
The update method is more efficient for complex state changes:
// ❌ Less efficient
const addItem = () => {
const current = items.value;
const newItem = { id: Date.now(), text: 'New item' };
items.set([...current, newItem]);
};
// ✅ More efficient
const addItem = () => {
items.update(current => [...current, { id: Date.now(), text: 'New item' }]);
};
Break down large state objects
Instead of one large state object, use multiple smaller refractions:
// ❌ Large state object
const app = lens.useRefraction({
user: null,
posts: [],
comments: [],
ui: { loading: false, error: null }
});
// ✅ Separate concerns
const user = lens.useRefraction(null);
const posts = lens.useRefraction([]);
const comments = lens.useRefraction([]);
const loading = lens.useRefraction(false);
const error = lens.useRefraction(null);
Common patterns
Loading states
const DataLoader = createComponent(({ lens, url }) => {
const data = lens.useRefraction(null);
const loading = lens.useRefraction(false);
const error = lens.useRefraction(null);
const fetchData = async () => {
loading.set(true);
error.set(null);
try {
const response = await fetch(url);
if (!response.ok) throw new Error('Failed to fetch');
const result = await response.json();
data.set(result);
} catch (err) {
error.set(err.message);
} finally {
loading.set(false);
}
};
lens.useEffect(() => {
fetchData();
}, [url]);
if (loading.value) return <div>Loading...</div>;
if (error.value) return <div>Error: {error.value}</div>;
if (!data.value) return <div>No data</div>;
return <div>Data: {JSON.stringify(data.value)}</div>;
});
Form handling
const ContactForm = createComponent(({ lens, onSubmit }) => {
const form = lens.useRefraction({
name: '',
email: '',
message: ''
});
const errors = lens.useRefraction({});
const updateField = (field, value) => {
form.update(current => ({ ...current, [field]: value }));
// Clear error when user starts typing
if (errors.value[field]) {
errors.update(current => {
const newErrors = { ...current };
delete newErrors[field];
return newErrors;
});
}
};
const validate = () => {
const newErrors = {};
if (!form.value.name.trim()) newErrors.name = 'Name is required';
if (!form.value.email.trim()) newErrors.email = 'Email is required';
if (!form.value.message.trim()) newErrors.message = 'Message is required';
errors.set(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e) => {
e.preventDefault();
if (validate()) {
onSubmit(form.value);
form.set({ name: '', email: '', message: '' });
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<input
value={form.value.name}
onChange={(e) => updateField('name', e.target.value)}
placeholder="Your name"
/>
{errors.value.name && <span className="error">{errors.value.name}</span>}
</div>
<div>
<input
value={form.value.email}
onChange={(e) => updateField('email', e.target.value)}
placeholder="Your email"
type="email"
/>
{errors.value.email && <span className="error">{errors.value.email}</span>}
</div>
<div>
<textarea
value={form.value.message}
onChange={(e) => updateField('message', e.target.value)}
placeholder="Your message"
/>
{errors.value.message && <span className="error">{errors.value.message}</span>}
</div>
<button type="submit">Send Message</button>
</form>
);
});
That's everything you need to know about managing state with refractions. Next, check out useOptic to learn about creating reusable logic patterns.