State Management
Refract's state management system centers around refractions—reactive units of state that automatically update your UI when data changes. This guide covers how to manage state effectively in Refract applications.
What are refractions?
A refraction is a reactive state container that holds a value and notifies components when that value changes. Unlike traditional state management where you manually trigger re-renders, refractions automatically update any component that uses their value.
import { useRefraction } from "refract";
const count = useRefraction(0);
console.log(count.value); // 0
count.set(5);
console.log(count.value); // 5
Local component state
Use lens.useRefraction() to create state that's scoped to a single component. This is ideal for form inputs, toggles, and other component-specific data.
import { createComponent } from "refract";
const LoginForm = createComponent(({ lens }) => {
const email = lens.useRefraction("");
const password = lens.useRefraction("");
const isSubmitting = lens.useRefraction(false);
const handleSubmit = async () => {
isSubmitting.set(true);
try {
await login(email.value, password.value);
} finally {
isSubmitting.set(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email.value}
onChange={(e) => email.set(e.target.value)}
/>
<input
type="password"
value={password.value}
onChange={(e) => password.set(e.target.value)}
/>
<button disabled={isSubmitting.value} type="submit">
{isSubmitting.value ? "Logging in..." : "Log in"}
</button>
</form>
);
});
Global state
Create refractions outside components to share state across your application. Global refractions persist throughout your app's lifetime.
// store/user.js
import { useRefraction } from "refract";
export const user = useRefraction(null);
export const isAuthenticated = useRefraction(false);
export function login(userData) {
user.set(userData);
isAuthenticated.set(true);
}
export function logout() {
user.set(null);
isAuthenticated.set(false);
}
// components/Header.js
import { createComponent } from "refract";
import { user, isAuthenticated, logout } from "../store/user.js";
const Header = createComponent(() => {
return (
<header>
{isAuthenticated.value ? (
<div>
Welcome, {user.value?.name}
<button onClick={logout}>Log out</button>
</div>
) : (
<button>Log in</button>
)}
</header>
);
});
Derived state
Create computed values that automatically update when their dependencies change using derived refractions.
import { useRefraction, derived } from "refract";
const todos = useRefraction([
{ id: 1, text: "Learn Refract", completed: false },
{ id: 2, text: "Build an app", completed: true }
]);
// Automatically recalculates when todos changes
const completedCount = derived(() =>
todos.value.filter(todo => todo.completed).length
);
const totalCount = derived(() => todos.value.length);
const progress = derived(() =>
totalCount.value === 0 ? 0 : completedCount.value / totalCount.value
);
Use derived state in components:
const TodoStats = createComponent(() => {
return (
<div>
<p>Completed: {completedCount.value} / {totalCount.value}</p>
<progress value={progress.value} max={1} />
</div>
);
});
Updating state
Refractions provide several methods for updating values:
Direct assignment with set()
const name = useRefraction("John");
name.set("Jane"); // Replace entire value
Functional updates with update()
Use update() when the new value depends on the current value:
const count = useRefraction(0);
count.update(current => current + 1);
const user = useRefraction({ name: "John", age: 30 });
user.update(current => ({ ...current, age: 31 }));
Batch updates
Group multiple state changes to prevent unnecessary re-renders:
import { batch } from "refract";
const firstName = useRefraction("");
const lastName = useRefraction("");
const email = useRefraction("");
function updateUser(userData) {
batch(() => {
firstName.set(userData.firstName);
lastName.set(userData.lastName);
email.set(userData.email);
});
// Components re-render only once after all updates
}
State patterns
Loading states
Track async operations with dedicated loading refractions:
const TodoList = createComponent(({ lens }) => {
const todos = lens.useRefraction([]);
const loading = lens.useRefraction(true);
const error = lens.useRefraction(null);
lens.useEffect(async () => {
try {
loading.set(true);
error.set(null);
const data = await fetchTodos();
todos.set(data);
} catch (err) {
error.set(err.message);
} finally {
loading.set(false);
}
}, []);
if (loading.value) return <div>Loading...</div>;
if (error.value) return <div>Error: {error.value}</div>;
return (
<ul>
{todos.value.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
});
Form state
Manage complex forms with nested state:
const ContactForm = createComponent(({ lens }) => {
const form = lens.useRefraction({
personal: {
firstName: "",
lastName: "",
email: ""
},
preferences: {
newsletter: false,
notifications: true
}
});
const updateField = (path, value) => {
form.update(current => {
const updated = { ...current };
// Simple nested property update
const keys = path.split('.');
let target = updated;
for (let i = 0; i < keys.length - 1; i++) {
target[keys[i]] = { ...target[keys[i]] };
target = target[keys[i]];
}
target[keys[keys.length - 1]] = value;
return updated;
});
};
return (
<form>
<input
value={form.value.personal.firstName}
onChange={(e) => updateField("personal.firstName", e.target.value)}
/>
<input
value={form.value.personal.lastName}
onChange={(e) => updateField("personal.lastName", e.target.value)}
/>
<input
type="checkbox"
checked={form.value.preferences.newsletter}
onChange={(e) => updateField("preferences.newsletter", e.target.checked)}
/>
</form>
);
});
State debugging
Refract provides built-in tools for debugging state changes:
State logging
Enable development mode to log all state changes:
// refract.config.js
export default {
development: {
logStateChanges: true,
logComponents: true
}
};
State inspection
Access refraction history in development:
const count = useRefraction(0);
// In development mode only
console.log(count._history); // Array of previous values
console.log(count._subscribers); // Components using this refraction
Best practices
Keep state minimal
Only store what you need in state. Derive other values using computed refractions:
// ❌ Storing redundant state
const todos = useRefraction([]);
const completedTodos = useRefraction([]); // Redundant
const pendingTodos = useRefraction([]); // Redundant
// ✅ Store minimal state, derive the rest
const todos = useRefraction([]);
const completedTodos = derived(() =>
todos.value.filter(todo => todo.completed)
);
const pendingTodos = derived(() =>
todos.value.filter(todo => !todo.completed)
);
Use TypeScript
Type your refractions for better developer experience:
interface User {
id: number;
name: string;
email: string;
}
const user = useRefraction<User | null>(null);
const users = useRefraction<User[]>([]);
Normalize complex state
For complex data structures, consider normalizing your state:
// ❌ Nested structure that's hard to update
const state = useRefraction({
posts: [
{ id: 1, title: "Post 1", author: { id: 1, name: "John" } },
{ id: 2, title: "Post 2", author: { id: 1, name: "John" } }
]
});
// ✅ Normalized structure
const users = useRefraction({
1: { id: 1, name: "John" }
});
const posts = useRefraction({
1: { id: 1, title: "Post 1", authorId: 1 },
2: { id: 2, title: "Post 2", authorId: 1 }
});
Colocate related state
Group related refractions together using custom hooks (optics):
// optics/useAuth.js
import { useRefraction } from "refract";
export function useAuth() {
const user = useRefraction(null);
const loading = useRefraction(false);
const error = useRefraction(null);
const login = async (credentials) => {
loading.set(true);
error.set(null);
try {
const userData = await authenticate(credentials);
user.set(userData);
} catch (err) {
error.set(err.message);
} finally {
loading.set(false);
}
};
const logout = () => {
user.set(null);
error.set(null);
};
return { user, loading, error, login, logout };
}