State Management
State management is at the heart of every reactive application. Refract provides a powerful yet simple state management system through refractions - reactive state primitives that automatically update your UI when values change. No Redux, no MobX, no complexity - just clarity.
Core Concepts
What is State?
State represents data that changes over time in your application:
- User input (forms, selections)
- Server data (API responses)
- UI state (modals, toggles, filters)
- Application state (authentication, preferences)
The Refract Approach
Refract's state management is built on three principles:
- Simplicity: State should be easy to create and update
- Reactivity: UI should automatically reflect state changes
- Locality: State should live close to where it's used
// This is all you need for reactive state
const Component = createComponent(({ lens }) => {
const count = lens.useRefraction(0);
return (
<button onClick={() => count.set(count.value + 1)}>
Clicked {count.value} times
</button>
);
});
Local State with Refractions
Creating State
Use lens.useRefraction() to create reactive state within components:
const TodoApp = createComponent(({ lens }) => {
// Primitive values
const text = lens.useRefraction("");
const count = lens.useRefraction(0);
const isActive = lens.useRefraction(true);
// Objects
const user = lens.useRefraction({
name: "John",
email: "john@example.com",
});
// Arrays
const todos = lens.useRefraction([
{ id: 1, text: "Learn Refract", done: false },
{ id: 2, text: "Build app", done: false },
]);
// Dates, Maps, Sets
const selectedDate = lens.useRefraction(new Date());
const cache = lens.useRefraction(new Map());
const tags = lens.useRefraction(new Set(["react", "refract"]));
return <div>{/* Your UI */}</div>;
});
Reading State
Access state values with the .value property:
const Counter = createComponent(({ lens }) => {
const count = lens.useRefraction(10);
return (
<div>
<p>Current count: {count.value}</p>
<p>Double: {count.value * 2}</p>
<p>Is even: {count.value % 2 === 0 ? "Yes" : "No"}</p>
</div>
);
});
Updating State
Use the .set() method to update state:
const Profile = createComponent(({ lens }) => {
const user = lens.useRefraction({ name: "", age: 0 });
const updateName = (newName) => {
// Replace entire object
user.set({ ...user.value, name: newName });
};
const incrementAge = () => {
// Update based on current value
user.set({ ...user.value, age: user.value.age + 1 });
};
return (
<div>
<input
value={user.value.name}
onChange={(e) => updateName(e.target.value)}
/>
<button onClick={incrementAge}>Birthday! (Age: {user.value.age})</button>
</div>
);
});
State Update Patterns
Common patterns for updating different types of state:
const StatePatterns = createComponent(({ lens }) => {
const todos = lens.useRefraction([]);
const filters = lens.useRefraction({ search: "", category: "all" });
const selected = lens.useRefraction(new Set());
// Array operations
const addTodo = (text) => {
todos.set([...todos.value, { id: Date.now(), text, done: false }]);
};
const removeTodo = (id) => {
todos.set(todos.value.filter((todo) => todo.id !== id));
};
const updateTodo = (id, updates) => {
todos.set(
todos.value.map((todo) =>
todo.id === id ? { ...todo, ...updates } : todo
)
);
};
// Object updates
const updateFilter = (key, value) => {
filters.set({ ...filters.value, [key]: value });
};
// Set operations
const toggleSelection = (id) => {
const newSet = new Set(selected.value);
if (newSet.has(id)) {
newSet.delete(id);
} else {
newSet.add(id);
}
selected.set(newSet);
};
return <div>{/* UI */}</div>;
});
Global State
Creating Global Refractions
Global state that persists across components:
// stores/auth.js
import { globalRefraction } from "refract-js";
export const currentUser = globalRefraction(null);
export const isAuthenticated = globalRefraction(false);
export const permissions = globalRefraction(new Set());
// stores/theme.js
export const theme = globalRefraction("light");
export const primaryColor = globalRefraction("#667eea");
// stores/app.js
export const notifications = globalRefraction([]);
export const isLoading = globalRefraction(false);
export const currentRoute = globalRefraction("/");
Using Global State
Import and use global refractions in any component:
import { currentUser, isAuthenticated } from "./stores/auth";
import { theme } from "./stores/theme";
const Header = createComponent(({ lens }) => {
// Global state works like local state
const user = currentUser;
const authenticated = isAuthenticated;
const currentTheme = theme;
const logout = () => {
currentUser.set(null);
isAuthenticated.set(false);
};
const toggleTheme = () => {
theme.set(theme.value === "light" ? "dark" : "light");
};
return (
<header className={`theme-${currentTheme.value}`}>
{authenticated.value ? (
<div>
Welcome, {user.value?.name}!<button onClick={logout}>Logout</button>
</div>
) : (
<button onClick={() => isAuthenticated.set(true)}>Login</button>
)}
<button onClick={toggleTheme}>🌓 Toggle Theme</button>
</header>
);
});
Global State Patterns
Organize global state with modules:
// stores/index.js
import { globalRefraction } from "refract-js";
// Create a store module
export const createStore = (name, initialState) => {
const state = globalRefraction(initialState);
return {
state,
// Add computed values
get value() {
return state.value;
},
// Add actions
reset: () => state.set(initialState),
// Add subscriptions
subscribe: (callback) => state.subscribe(callback),
};
};
// Create stores
export const cartStore = createStore("cart", {
items: [],
total: 0,
});
export const userStore = createStore("user", {
profile: null,
preferences: {},
session: null,
});
// Usage in components
const Cart = createComponent(({ lens }) => {
const cart = cartStore.state;
const addItem = (product) => {
const newItems = [...cart.value.items, product];
const total = newItems.reduce((sum, item) => sum + item.price, 0);
cart.set({ items: newItems, total });
};
return (
<div>
<p>Items: {cart.value.items.length}</p>
<p>Total: ${cart.value.total}</p>
</div>
);
});
Derived State
Computed Values
Create values that automatically update when dependencies change:
const ShoppingCart = createComponent(({ lens }) => {
const items = lens.useRefraction([
{ id: 1, name: "Laptop", price: 999, quantity: 1 },
{ id: 2, name: "Mouse", price: 29, quantity: 2 },
]);
const taxRate = lens.useRefraction(0.08);
// Computed values - automatically update
const subtotal = lens.useComputed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
);
const tax = lens.useComputed(() => subtotal.value * taxRate.value);
const total = lens.useComputed(() => subtotal.value + tax.value);
return (
<div>
<h3>Shopping Cart</h3>
<p>Subtotal: ${subtotal.value.toFixed(2)}</p>
<p>
Tax ({(taxRate.value * 100).toFixed(0)}%): ${tax.value.toFixed(2)}
</p>
<p>Total: ${total.value.toFixed(2)}</p>
</div>
);
});
Derived Refractions
Create refractions that derive from other refractions:
import { derivedRefraction } from "refract-js";
// Base refractions
const firstName = globalRefraction("John");
const lastName = globalRefraction("Doe");
// Derived refraction
const fullName = derivedRefraction(
[firstName, lastName],
([first, last]) => `${first} ${last}`
);
// Bidirectional derived state
const celsius = globalRefraction(0);
const fahrenheit = derivedRefraction(
[celsius],
([c]) => (c * 9) / 5 + 32,
(f) => celsius.set(((f - 32) * 5) / 9) // Optional setter
);
const TemperatureConverter = createComponent(({ lens }) => {
return (
<div>
<input
type="number"
value={celsius.value}
onChange={(e) => celsius.set(Number(e.target.value))}
/>
°C =
<input
type="number"
value={fahrenheit.value}
onChange={(e) => fahrenheit.set(Number(e.target.value))}
/>
°F
</div>
);
});
Advanced State Patterns
State Machines
Implement finite state machines for complex logic:
const createStateMachine = (config) => {
const currentState = globalRefraction(config.initial);
const transition = (event) => {
const state = currentState.value;
const nextState = config.states[state]?.on?.[event];
if (nextState) {
// Run exit action
config.states[state]?.exit?.();
// Transition
currentState.set(nextState);
// Run entry action
config.states[nextState]?.entry?.();
}
};
return { currentState, transition };
};
// Define state machine
const formMachine = createStateMachine({
initial: "idle",
states: {
idle: {
on: { START: "filling" },
entry: () => console.log("Form ready"),
},
filling: {
on: {
SUBMIT: "validating",
CANCEL: "idle",
},
},
validating: {
on: {
VALID: "submitting",
INVALID: "filling",
},
entry: () => console.log("Validating..."),
},
submitting: {
on: {
SUCCESS: "complete",
ERROR: "filling",
},
entry: () => console.log("Submitting..."),
},
complete: {
entry: () => console.log("Form submitted!"),
},
},
});
const Form = createComponent(({ lens }) => {
const { currentState, transition } = formMachine;
return (
<div>
<p>State: {currentState.value}</p>
<button onClick={() => transition("START")}>Start</button>
<button onClick={() => transition("SUBMIT")}>Submit</button>
{/* Form UI based on state */}
</div>
);
});
Undo/Redo
Implement undo/redo functionality:
const createUndoable = (initialValue) => {
const history = globalRefraction([initialValue]);
const currentIndex = globalRefraction(0);
const current = derivedRefraction(
[history, currentIndex],
([hist, idx]) => hist[idx]
);
const set = (newValue) => {
// Remove any future history
const newHistory = history.value.slice(0, currentIndex.value + 1);
newHistory.push(newValue);
history.set(newHistory);
currentIndex.set(newHistory.length - 1);
};
const undo = () => {
if (currentIndex.value > 0) {
currentIndex.set(currentIndex.value - 1);
}
};
const redo = () => {
if (currentIndex.value < history.value.length - 1) {
currentIndex.set(currentIndex.value + 1);
}
};
const canUndo = derivedRefraction([currentIndex], ([idx]) => idx > 0);
const canRedo = derivedRefraction(
[currentIndex, history],
([idx, hist]) => idx < hist.length - 1
);
return { current, set, undo, redo, canUndo, canRedo };
};
const TextEditor = createComponent(({ lens }) => {
const editor = createUndoable("");
return (
<div>
<div>
<button onClick={editor.undo} disabled={!editor.canUndo.value}>
↶ Undo
</button>
<button onClick={editor.redo} disabled={!editor.canRedo.value}>
↷ Redo
</button>
</div>
<textarea
value={editor.current.value}
onChange={(e) => editor.set(e.target.value)}
rows={10}
cols={50}
/>
</div>
);
});
Optimistic Updates
Handle optimistic UI updates for better UX:
const createOptimisticState = (initialValue, updateFn) => {
const actualValue = globalRefraction(initialValue);
const optimisticValue = globalRefraction(initialValue);
const pending = globalRefraction(false);
const update = async (newValue) => {
// Optimistically update UI
optimisticValue.set(newValue);
pending.set(true);
try {
// Perform actual update
const result = await updateFn(newValue);
actualValue.set(result);
optimisticValue.set(result);
} catch (error) {
// Revert on error
optimisticValue.set(actualValue.value);
throw error;
} finally {
pending.set(false);
}
};
return {
value: optimisticValue,
pending,
update,
};
};
const TodoList = createComponent(({ lens }) => {
const todos = createOptimisticState([], async (newTodos) => {
const response = await fetch("/api/todos", {
method: "POST",
body: JSON.stringify(newTodos),
});
return response.json();
});
const addTodo = (text) => {
const newTodos = [
...todos.value.value,
{
id: Date.now(),
text,
done: false,
},
];
todos.update(newTodos);
};
return (
<div>
{todos.pending.value && <p>Saving...</p>}
{todos.value.value.map((todo) => (
<div key={todo.id}>{todo.text}</div>
))}
</div>
);
});
State Synchronization
Local Storage Sync
Automatically sync state with localStorage:
const createPersistedRefraction = (key, initialValue) => {
// Load from localStorage
const stored = localStorage.getItem(key);
const initial = stored ? JSON.parse(stored) : initialValue;
const refraction = globalRefraction(initial);
// Save to localStorage on change
refraction.subscribe((value) => {
localStorage.setItem(key, JSON.stringify(value));
});
return refraction;
};
// Usage
const preferences = createPersistedRefraction("user-preferences", {
theme: "light",
language: "en",
notifications: true,
});
const Settings = createComponent(({ lens }) => {
return (
<div>
<label>
<input
type="checkbox"
checked={preferences.value.notifications}
onChange={(e) =>
preferences.set({
...preferences.value,
notifications: e.target.checked,
})
}
/>
Enable notifications
</label>
</div>
);
});
URL State Sync
Sync state with URL parameters:
const createURLRefraction = (param, initialValue) => {
const url = new URL(window.location);
const initial = url.searchParams.get(param) || initialValue;
const refraction = globalRefraction(initial);
refraction.subscribe((value) => {
const url = new URL(window.location);
url.searchParams.set(param, value);
window.history.replaceState({}, "", url);
});
// Listen for browser navigation
window.addEventListener("popstate", () => {
const url = new URL(window.location);
const value = url.searchParams.get(param);
if (value !== null) refraction.set(value);
});
return refraction;
};
// Usage
const searchQuery = createURLRefraction("q", "");
const currentPage = createURLRefraction("page", "1");
const SearchResults = createComponent(({ lens }) => {
return (
<div>
<input
type="search"
value={searchQuery.value}
onChange={(e) => searchQuery.set(e.target.value)}
placeholder="Search..."
/>
<p>Page {currentPage.value}</p>
<button
onClick={() => currentPage.set(String(Number(currentPage.value) + 1))}
>
Next Page
</button>
</div>
);
});
WebSocket State Sync
Real-time state synchronization:
const createRealtimeRefraction = (channel, initialValue) => {
const refraction = globalRefraction(initialValue);
const ws = new WebSocket(`wss://api.example.com/${channel}`);
// Receive updates
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
refraction.set(data);
};
// Send updates
const originalSet = refraction.set;
refraction.set = (value) => {
originalSet(value);
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(value));
}
};
return refraction;
};
// Usage
const collaborativeDoc = createRealtimeRefraction("doc/123", {
content: "",
users: [],
});
const CollaborativeEditor = createComponent(({ lens }) => {
return (
<div>
<div>Active users: {collaborativeDoc.value.users.length}</div>
<textarea
value={collaborativeDoc.value.content}
onChange={(e) =>
collaborativeDoc.set({
...collaborativeDoc.value,
content: e.target.value,
})
}
/>
</div>
);
});
State DevTools
Debugging State
Enable state debugging in development:
// Enable debug mode
if (process.env.NODE_ENV === "development") {
window.__REFRACT_DEBUG__ = true;
// Log all state changes
globalRefraction.prototype.set = function (value) {
console.log(`[State Change] ${this.name}:`, {
old: this.value,
new: value,
stack: new Error().stack,
});
this._set(value);
};
}
// Name your refractions for better debugging
const userState = globalRefraction(null);
userState.name = "userState";
const cartState = globalRefraction([]);
cartState.name = "cartState";
State Inspector
Create a state inspector component:
const StateInspector = createComponent(({ lens }) => {
const states = lens.useRefraction({});
lens.useEffect(() => {
// Collect all global refractions
const refractions = window.__REFRACT_REFRACTIONS__ || {};
states.set(refractions);
// Update on changes
const interval = setInterval(() => {
const current = {};
Object.entries(refractions).forEach(([key, ref]) => {
current[key] = ref.value;
});
states.set(current);
}, 100);
return () => clearInterval(interval);
}, []);
return (
<div
style={{
position: "fixed",
bottom: 0,
right: 0,
background: "black",
color: "green",
padding: "10px",
fontFamily: "monospace",
maxHeight: "300px",
overflow: "auto",
}}
>
<h3>State Inspector</h3>
<pre>{JSON.stringify(states.value, null, 2)}</pre>
</div>
);
});
Best Practices
1. Keep State Minimal
Only store what you can't derive:
// ❌ Bad - storing derived values
const state = lens.useRefraction({
items: [],
count: 0, // Can be derived
isEmpty: true, // Can be derived
total: 0, // Can be derived
});
// ✅ Good - derive what you can
const items = lens.useRefraction([]);
const count = items.value.length;
const isEmpty = items.value.length === 0;
const total = items.value.reduce((sum, item) => sum + item.price, 0);
2. Co-locate State
Keep state close to where it's used:
// ❌ Bad - global state for local concerns
const globalModalOpen = globalRefraction(false);
// ✅ Good - local state for local UI
const Modal = createComponent(({ lens }) => {
const isOpen = lens.useRefraction(false);
// Modal logic
});
3. Normalize Complex State
Structure state for easy updates:
// ❌ Bad - nested arrays
const state = lens.useRefraction({
users: [
{ id: 1, name: 'John', posts: [...] }
]
})
// ✅ Good - normalized structure
const state = lens.useRefraction({
users: { 1: { id: 1, name: 'John' } },
posts: { 1: { id: 1, userId: 1, text: '...' } },
userPosts: { 1: [1, 2, 3] }
})
4. Use Immutable Updates
Always create new objects/arrays:
// ❌ Bad - mutating state
state.value.push(newItem);
state.set(state.value);
// ✅ Good - immutable update
state.set([...state.value, newItem]);
Common Patterns
Form State Management
const useForm = (initialValues = {}) => {
const values = useRefraction(initialValues);
const errors = useRefraction({});
const touched = useRefraction({});
const submitting = useRefraction(false);
const setFieldValue = (name, value) => {
values.set({ ...values.value, [name]: value });
};
const setFieldError = (name, error) => {
errors.set({ ...errors.value, [name]: error });
};
const setFieldTouched = (name) => {
touched.set({ ...touched.value, [name]: true });
};
const handleSubmit = async (onSubmit) => {
submitting.set(true);
try {
await onSubmit(values.value);
} finally {
submitting.set(false);
}
};
return {
values,
errors,
touched,
submitting,
setFieldValue,
setFieldError,
setFieldTouched,
handleSubmit,
};
};
Async State Pattern
const useAsync = (asyncFn) => {
const data = useRefraction(null);
const loading = useRefraction(false);
const error = useRefraction(null);
const execute = async (...args) => {
loading.set(true);
error.set(null);
try {
const result = await asyncFn(...args);
data.set(result);
return result;
} catch (err) {
error.set(err);
throw err;
} finally {
loading.set(false);
}
};
return { data, loading, error, execute };
};
🚀 Performance Tips
- Use computed values for derived state
- Batch updates when modifying multiple states
- Debounce rapid state changes
- Lazy initialize expensive state
- Use selectors to minimize re-renders
Next Steps
Now that you understand state management in Refract:
- Learn about Side Effects
- Explore Optical Composition
- Master Data Fetching
- Optimize with Performance Guide
State management doesn't have to be complex. With Refract's refractions, it's simple, predictable, and powerful.