Components
Components are the building blocks of Refract applications. They are pure functions that describe what the UI should look like based on the current state. Unlike traditional frameworks, Refract components are created using the createComponent() function and receive reactive capabilities through the lens system.
Creating Components
Basic Component Structure
import { createComponent } from 'refract';
const MyComponent = createComponent(({ lens, ...props }) => {
// Component logic here
return (
<div>
<h1>Hello, Refract!</h1>
</div>
);
});
export default MyComponent;
Component with Props
const Greeting = createComponent(({ lens, name, age }) => {
return (
<div className="greeting">
<h2>Hello, {name}!</h2>
<p>You are {age} years old.</p>
</div>
);
});
// Usage
<Greeting name="Alice" age={25} />
Component Lifecycle
Refract components have a simplified lifecycle compared to class-based components:
Mount Phase
When a component is first created and added to the DOM:
const LifecycleExample = createComponent(({ lens }) => {
const data = lens.useRefraction(null);
// Runs once when component mounts
lens.useEffect(() => {
console.log('Component mounted');
fetchData().then(data.set);
}, []); // Empty dependency array = mount only
return <div>{data.value ? 'Data loaded!' : 'Loading...'}</div>;
});
Update Phase
When props or state change:
const UpdateExample = createComponent(({ lens, userId }) => {
const user = lens.useRefraction(null);
// Runs when userId prop changes
lens.useEffect(() => {
console.log('Fetching user:', userId);
fetchUser(userId).then(user.set);
}, [userId]); // Dependency array includes userId
return <div>{user.value?.name || 'Loading user...'}</div>;
});
Cleanup Phase
When a component is removed from the DOM:
const CleanupExample = createComponent(({ lens }) => {
lens.useEffect(() => {
const timer = setInterval(() => {
console.log('Timer tick');
}, 1000);
// Cleanup function
return () => {
console.log('Component unmounting');
clearInterval(timer);
};
}, []);
return <div>Timer running...</div>;
});
State Management in Components
Local State with Refractions
const Counter = createComponent(({ lens }) => {
const count = lens.useRefraction(0);
const step = lens.useRefraction(1);
const increment = () => count.set(count.value + step.value);
const decrement = () => count.set(count.value - step.value);
return (
<div>
<h3>Count: {count.value}</h3>
<input
type="number"
value={step.value}
onChange={(e) => step.set(Number(e.target.value))}
placeholder="Step size"
/>
<button onClick={decrement}>-{step.value}</button>
<button onClick={increment}>+{step.value}</button>
</div>
);
});
Computed Values
const ShoppingCart = createComponent(({ lens }) => {
const items = lens.useRefraction([
{ id: 1, name: 'Apple', price: 1.50, quantity: 2 },
{ id: 2, name: 'Banana', price: 0.75, quantity: 3 }
]);
// Computed value using useOptic
const total = lens.useOptic(() => {
return items.value.reduce((sum, item) =>
sum + (item.price * item.quantity), 0
);
}, [items.value]);
return (
<div>
<h3>Shopping Cart</h3>
{items.value.map(item => (
<div key={item.id}>
{item.name} - ${item.price} x {item.quantity}
</div>
))}
<h4>Total: ${total.toFixed(2)}</h4>
</div>
);
});
Component Composition
Parent-Child Communication
// Child component
const TodoItem = createComponent(({ lens, todo, onToggle, onDelete }) => {
return (
<div className={`todo-item ${todo.completed ? 'completed' : ''}`}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</div>
);
});
// Parent component
const TodoList = createComponent(({ lens }) => {
const todos = lens.useRefraction([
{ id: 1, text: 'Learn Refract', completed: false },
{ id: 2, text: 'Build an app', completed: false }
]);
const toggleTodo = (id) => {
todos.set(todos.value.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
const deleteTodo = (id) => {
todos.set(todos.value.filter(todo => todo.id !== id));
};
return (
<div>
<h2>Todo List</h2>
{todos.value.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
))}
</div>
);
});
Higher-Order Components (HOCs)
// HOC for adding loading state
const withLoading = (WrappedComponent) => {
return createComponent((props) => {
const { lens, isLoading, ...otherProps } = props;
if (isLoading) {
return <div className="loading">Loading...</div>;
}
return <WrappedComponent lens={lens} {...otherProps} />;
});
};
// Usage
const DataDisplay = createComponent(({ lens, data }) => {
return <div>{JSON.stringify(data)}</div>;
});
const DataDisplayWithLoading = withLoading(DataDisplay);
// In parent component
<DataDisplayWithLoading
isLoading={!data.value}
data={data.value}
/>
Component Patterns
Render Props Pattern
const DataFetcher = createComponent(({ lens, url, children }) => {
const data = lens.useRefraction(null);
const loading = lens.useRefraction(true);
const error = lens.useRefraction(null);
lens.useEffect(() => {
loading.set(true);
fetch(url)
.then(response => response.json())
.then(result => {
data.set(result);
loading.set(false);
})
.catch(err => {
error.set(err);
loading.set(false);
});
}, [url]);
return children({
data: data.value,
loading: loading.value,
error: error.value
});
});
// Usage
<DataFetcher url="/api/users">
{({ data, loading, error }) => {
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <UserList users={data} />;
}}
</DataFetcher>
Compound Components
const Tabs = createComponent(({ lens, children, defaultTab = 0 }) => {
const activeTab = lens.useRefraction(defaultTab);
return (
<div className="tabs">
{children.map((child, index) =>
React.cloneElement(child, {
isActive: index === activeTab.value,
onActivate: () => activeTab.set(index)
})
)}
</div>
);
});
const Tab = createComponent(({ lens, title, children, isActive, onActivate }) => {
return (
<div className="tab">
<button
className={`tab-header ${isActive ? 'active' : ''}`}
onClick={onActivate}
>
{title}
</button>
{isActive && (
<div className="tab-content">
{children}
</div>
)}
</div>
);
});
// Usage
<Tabs defaultTab={0}>
<Tab title="Profile">
<UserProfile />
</Tab>
<Tab title="Settings">
<UserSettings />
</Tab>
</Tabs>
Performance Optimization
Memoization
import { memo } from 'refract';
const ExpensiveComponent = memo(createComponent(({ lens, data }) => {
// Expensive computation
const processedData = lens.useOptic(() => {
return data.map(item => ({
...item,
processed: heavyComputation(item)
}));
}, [data]);
return (
<div>
{processedData.map(item => (
<div key={item.id}>{item.processed}</div>
))}
</div>
);
}));
Lazy Loading
import { lazy, Suspense } from 'refract';
const LazyComponent = lazy(() => import('./HeavyComponent'));
const App = createComponent(({ lens }) => {
return (
<div>
<h1>My App</h1>
<Suspense fallback={<div>Loading heavy component...</div>}>
<LazyComponent />
</Suspense>
</div>
);
});
Best Practices
1. Keep Components Pure
// ✅ Good - Pure component
const PureComponent = createComponent(({ lens, name }) => {
return <h1>Hello, {name}!</h1>;
});
// ❌ Bad - Side effects in render
const ImpureComponent = createComponent(({ lens, name }) => {
console.log('Rendering...'); // Side effect!
localStorage.setItem('name', name); // Side effect!
return <h1>Hello, {name}!</h1>;
});
2. Use Descriptive Names
// ✅ Good
const UserProfileCard = createComponent(({ lens, user }) => {
// ...
});
// ❌ Bad
const Component1 = createComponent(({ lens, data }) => {
// ...
});
3. Extract Complex Logic
// ✅ Good - Logic extracted to custom optic
const useUserData = (userId) => {
const user = useRefraction(null);
const loading = useRefraction(true);
useEffect(() => {
fetchUser(userId).then(user.set).finally(() => loading.set(false));
}, [userId]);
return { user: user.value, loading: loading.value };
};
const UserProfile = createComponent(({ lens, userId }) => {
const { user, loading } = lens.useOptic(() => useUserData(userId), [userId]);
if (loading) return <div>Loading...</div>;
return <div>{user.name}</div>;
});
4. Handle Edge Cases
const SafeComponent = createComponent(({ lens, items = [] }) => {
if (!items.length) {
return <div>No items to display</div>;
}
return (
<ul>
{items.map(item => (
<li key={item.id || item.name}>
{item.name || 'Unnamed item'}
</li>
))}
</ul>
);
});
Next Steps
Now that you understand components, learn about:
- Refractions - Reactive state management
- Lenses - Scoped access to reactive features
- Optics - Reusable logic patterns