Skip to main content

Refractions

Refractions are the beating heart of Refract applications. They're reactive containers that hold your data and automatically update your UI whenever that data changes without manual subscriptions or complex state management, just pure reactivity.

Think of a refraction like a smart box that not only holds your data but also knows exactly which parts of your UI need to update when the data changes. It's like having a personal assistant for each piece of state in your app.

Why Refractions?​

Traditional state management requires you to manually connect your data to your UI. You have to remember to update components, manage subscriptions, and handle complex state synchronization. Refractions remove all this complexity.

When you change a refraction's value anywhere in your app, every component that uses that refraction automatically re-renders with the new data. It's like having a direct wire between your data and your UI - change the data, and the UI instantly reflects that change.

What Makes Refractions Special?​

  1. Automatic Reactivity: When a refraction's value changes, all components using that refraction automatically re-render with the new value.

  2. Zero Boilerplate: No need to set up subscriptions, dispatch actions, or manage complex state trees.

  3. Cross-Component Sharing: The same refraction can be used across multiple components, and they all stay in sync automatically.

  4. Built-in Performance: Refractions only update components that actually use the changed data.

How to create refractions​

There are two main ways to create refractions, each serving different purposes in your application:

1. Local Refractions (Component-Scoped)​

Local refractions live inside a specific component and are perfect for component-specific state like form inputs, toggles, or local UI state.

Step-by-Step: Creating Local Refractions
import { createComponent } from "refract";

const Counter = createComponent(({ lens }) => {
// Step 1: Create a refraction with an initial value
const count = lens.useRefraction(0);
// ↑ This creates a reactive container holding the number 0

// Step 2: Create another refraction for a different piece of state
const message = lens.useRefraction("Hello, Refract!");
// ↑ This creates a reactive container holding a string

// Step 3: Use the refractions in your JSX
return (
<div>
{/* Access the value using .value */}
<h2>{message.value}</h2>
<p>Count: {count.value}</p>

{/* Update the value using .set() */}
<button onClick={() => count.set(count.value + 1)}>Increment</button>
<button onClick={() => message.set("Updated message!")}>
Change Message
</button>
</div>
);
});

What happens here:

  1. lens.useRefraction(0) - Creates a reactive container with initial value 0
  2. count.value - Reads the current value from the refraction
  3. count.set(newValue) - Updates the refraction's value
  4. Automatic re-render - When you call .set(), the component automatically re-renders with the new value

2. Global Refractions (Application-Wide)​

Global refractions are created outside of components and can be shared across your entire app. They're perfect for user data, theme settings, or any state that multiple components need to access.

Step 1: Create Global Refractions​

First, create your global refractions in a separate file:

store.js - Creating Global Refractions
import { createRefraction } from "refract";

// Step 1: Create global refractions with initial values
export const globalTheme = createRefraction("light");
// ↑ This creates a global reactive container for theme
export const globalUser = createRefraction(null);
// ↑ This creates a global reactive container for user data
export const globalNotifications = createRefraction([]);
// ↑ This creates a global reactive container for notifications

Step 2: Use Global Refractions in Components​

Now any component can connect to these global refractions:

ThemeToggle.js - Using Global Refractions
import { createComponent } from "refract";
import { globalTheme } from "./store.js";

const ThemeToggle = createComponent(({ lens }) => {
// Step 2: Connect to the global refraction
const theme = lens.useRefraction(globalTheme);
// ↑ This connects this component to the global theme refraction

const toggleTheme = () => {
// Step 3: Update the global refraction
theme.set(theme.value === "light" ? "dark" : "light");
// ↑ This updates the global theme, and ALL components using it will re-render
};

return (
<button onClick={toggleTheme}>
Current theme: {theme.value}
Switch to {theme.value === "light" ? "dark" : "light"}
</button>
);
});

Step 3: Multiple Components Stay in Sync​

Here's the magic - any component that uses the same global refraction automatically stays in sync:

App.js - Multiple Components Using Same Global State
import { createComponent } from "refract";
import { globalTheme } from "./store.js";
import ThemeToggle from "./ThemeToggle.js";

const App = createComponent(({ lens }) => {
// Step 4: This component also connects to the same global refraction
const theme = lens.useRefraction(globalTheme);
// ↑ Now both App and ThemeToggle are connected to the same global state

return (
<div className={`app theme-${theme.value}`}>
<h1>My App</h1>
<ThemeToggle />
{/* When ThemeToggle changes the theme, this text updates automatically! */}
<p>Current theme: {theme.value}</p>
</div>
);
});

What happens here:

  1. Both components connect to the same globalTheme refraction
  2. When ThemeToggle updates the theme by calling theme.set()
  3. Both components automatically re-render with the new theme value
  4. No manual synchronization needed - it just works!

This is the power of global refractions - shared reactive state with zero boilerplate.

Working with Complex State​

Refractions aren't limited to simple values - they can hold objects, arrays, or any complex data structure. Here's how to work with them effectively.

Step-by-Step: Managing Object State​

  1. Create a refraction with an object as initial value:
Managing Complex Objects
const UserProfile = createComponent(({ lens }) => {

const user = lens.useRefraction({
name: "John Doe",
email: "john@example.com",
preferences: {
theme: "dark",
notifications: true,
},
});
// ↑ This creates a reactive container holding the entire user object
  1. Create a function to update part of the object
  const updateUserName = (newName) => {
// IMPORTANT: Always create a new object for reactivity
user.set({
...user.value, // ← Spread the existing object
name: newName, // ← Override just the name property
});
// ↑ This creates a new object, which triggers reactivity
};
  1. Access nested properties using .value :
  return (
<div>
<h2>Welcome, {user.value.name}!</h2>
<p>Email: {user.value.email}</p>
<p>Theme: {user.value.preferences.theme}</p>

<button onClick={() => updateUserName("Jane Doe")}>Change Name</button>
</div>
);
});

Step-by-Step: Managing Array State​

  1. Create a refraction with an array as initial value
Managing Arrays
const TodoList = createComponent(({ lens }) => {
const todos = lens.useRefraction([
{ id: 1, text: "Learn Refract", completed: false },
{ id: 2, text: "Build an app", completed: false },
]);
// ↑ This creates a reactive container holding the entire array

  1. Create a function to add items to the array:
  const addTodo = (text) => {
const newTodo = {
id: Date.now(),
text,
completed: false,
};
// IMPORTANT: Always create a new array for reactivity
todos.set([...todos.value, newTodo]);
// ↑ Spread existing array ↑ Add new item
// This creates a new array, which triggers reactivity
};
  1. Create a function to update an item in the array
  const toggleTodo = (id) => {
todos.set(
todos.value.map(
(todo) =>
todo.id === id
? { ...todo, completed: !todo.completed } // ← Create new object
: todo // ← Keep existing object
)
);
// ↑ map() creates a new array, which triggers reactivity
};
  1. Step 4: Render the array using .value:
  return (
<div>
<h3>Todos ({todos.value.length})</h3>
{todos.value.map((todo) => (
<div key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span
style={{
textDecoration: todo.completed ? "line-through" : "none",
}}
>
{todo.text}
</span>
</div>
))}

<button onClick={() => addTodo("New task")}>Add Todo</button>
</div>
);
});

Key Principles for Complex State​

🔑 Always Create New Objects/Arrays

// ✅ CORRECT - Creates new object/array
user.set({ ...user.value, name: "New Name" });
todos.set([...todos.value, newItem]);

// ❌ WRONG - Mutates existing object/array
user.value.name = "New Name"; // Won't trigger reactivity!
todos.value.push(newItem); // Won't trigger reactivity!

🔑 Why New Objects Matter

Refract uses object identity to detect changes. When you create a new object or array, Refract knows something changed and updates all components using that refraction. If you mutate the existing object, Refract doesn't know anything changed.

Best Practices​

1. Use Local State by Default​

Start with local refractions and only move to global state when you need to share data across components:

Choosing Local vs Global State
// ✅ Good - local state for component-specific data
const Modal = createComponent(({ lens }) => {
const isOpen = lens.useRefraction(false); // Only this modal needs this

return (
<div>
<button onClick={() => isOpen.set(true)}>Open Modal</button>
{isOpen.value && <div className="modal">Modal content</div>}
</div>
);
});

// ✅ Good - global state for shared data
const globalUser = createRefraction(null);

const Header = createComponent(({ lens }) => {
const user = lens.useRefraction(globalUser); // Shared across app
return <div>Welcome, {user.value?.name}</div>;
});

When to use local vs global:

  • Local: Component-specific UI state (modals, forms, toggles)
  • Global: Data shared across multiple components (user, theme, cart)

2. Keep Updates Immutable​

Always create new objects and arrays when updating complex state. Use helper functions to make this easier:

Immutable Update Helpers
// Helper functions for common update patterns
const updateObject = (obj, updates) => ({ ...obj, ...updates });

const updateArray = (array, index, updates) =>
array.map((item, i) => (i === index ? { ...item, ...updates } : item));

const removeFromArray = (array, index) => array.filter((_, i) => i !== index);

// Usage
user.set(updateObject(user.value, { name: "New Name" }));
items.set(updateArray(items.value, 0, { completed: true }));

3. Organize Global State​

Keep global refractions organized in dedicated files:

import { createRefraction } from "refract";

export const currentUser = createRefraction(null);
export const isAuthenticated = createRefraction(false);
export const authToken = createRefraction(null);