Optical Composition Patterns
The Art of Optical Composition
So far, you’ve learned the basics of Optics—our way of packaging reusable logic and reactive behaviors. But once your app grows beyond a few components, you’ll quickly find out that composing optics together is where the real magic happens.
If you've been using optics as solo instruments - like our mouse tracker or theme switcher - get ready to conduct an entire orchestra. Optical composition is where individual optics combine to create something greater than the sum of their parts.
Think of this as Lego for app logic: small blocks (optics) that can be combined into bigger, more powerful structures.
Why Not Just Reuse? Why Compose?
When you start building features like authentication, theme switching, or live counters, you’ll notice patterns repeating themselves. Rather than duplicating code, optics let you reuse, extend, and compose logic in clean ways:
- Reusability: One optic can be shared across multiple components.
- Isolation: Each optic handles its own slice of state/effects.
- Scalability: Large features become easier to manage by stitching smaller optics.
If you’re not familiar with optics yet, check out the Optics introduction first.
A Simple Example: Counter + Theme Toggle
Let’s start small. Imagine you already have:
- A Counter Optic for incrementing numbers.
- A Theme Optic for switching between light/dark mode.
On their own, they look like this:
// counterOptic.js
import { createOptic, useRefraction } from '@refract-framework/core';
export const counterOptic = createOptic(() => {
const [count, setCount] = useRefraction(0);
return {
count,
increment: () => setCount(c => c + 1),
reset: () => setCount(0),
};
});
// themeOptic.js
import { createOptic, useRefraction } from '@refract-framework/core';
export const themeOptic = createOptic(() => {
const [theme, setTheme] = useRefraction('light');
return {
theme,
toggle: () => setTheme(t => (t === 'light' ? 'dark' : 'light')),
};
});
Composing Them Together
Here’s where composition shines: Instead of wiring both optics separately inside every component, we can compose them into one higher-order optic:
// appOptic.js
import { createOptic, composeOptics } from '@refract-framework/core';
import { counterOptic } from './counterOptic';
import { themeOptic } from './themeOptic';
export const appOptic = composeOptics({
counter: counterOptic,
theme: themeOptic,
});
Now, inside a component, you can access both in one shot:
import { createComponent } from '@refract-framework/core';
import { appOptic } from './appOptic';
const Dashboard = createComponent(() => {
const { counter, theme } = appOptic.use();
return (
<div>
<h2>Count: {counter.count}</h2>
<button onClick={counter.increment}>+1</button>
<button onClick={counter.reset}>Reset</button>
<h3>Theme: {theme.theme}</h3>
<button onClick={theme.toggle}>Toggle Theme</button>
</div>
);
});
Boom! You just stitched together two optics into one clean dashboard.
Composition Patterns
Let’s explore some of the most common optical composition patterns you’ll use in real-world Refract apps.
Each one starts simple but can be combined for incredibly sophisticated results.
Pattern 1: The Mapper - Transforming Optical Output
The simplest composition pattern transforms optical values as they flow through your system.
const useEnhancedMouse = () => {
const { position } = useMousePosition();
// Transform raw coordinates into useful data
const normalizedPosition = useOptic(() => ({
x: Math.floor(position.value.x / window.innerWidth * 100),
y: Math.floor(position.value.y / window.innerHeight * 100),
isInViewport: position.value.x > 0 && position.value.y > 0
}), [position]);
return { position: normalizedPosition };
};
- Converting raw values to percentages
- Adding derived properties
- Normalizing data across different units
- Creating boolean flags from complex conditions
Pattern 2: The Combiner - Merging Multiple Optics
Combine multiple optics to create coordinated state that updates when any source changes.
const useScrollTracker = () => {
const { position: mousePos } = useMousePosition();
const { position: scrollPos } = useScrollPosition();
const combined = useOptic(() => ({
mouse: mousePos.value,
scroll: scrollPos.value,
totalX: mousePos.value.x + scrollPos.value.x,
totalY: mousePos.value.y + scrollPos.value.y,
isScrolling: scrollPos.value.y > 0
}), [mousePos, scrollPos]);
return combined;
};
Pattern 3: The Filter - Conditional Optical Flow
Create optics that only update when specific conditions are met, reducing unnecessary re-renders.
const useThrottledMouse = (delay = 100) => {
const { position } = useMousePosition();
const [lastUpdate, setLastUpdate] = useRefraction(Date.now());
const throttled = useOptic(() => {
const now = Date.now();
if (now - lastUpdate.value > delay) {
setLastUpdate(now);
return position.value;
}
return null;
}, [position, lastUpdate]);
return { position: throttled };
};
Filtering patterns are crucial for high-frequency events like mouse movement or animation frames. Learn more in our Performance Optimization guide.
Pattern 4: The Coordinator - Multi-Optic Synchronization
Coordinate multiple optics that need to update together in specific sequences.
const useAnimationSequence = () => {
const [currentStep, setCurrentStep] = useRefraction(0);
const [isPlaying, setIsPlaying] = useRefraction(false);
const sequence = useOptic(() => ({
step: currentStep.value,
playing: isPlaying.value,
progress: (currentStep.value / totalSteps) * 100,
canPlay: currentStep.value < totalSteps,
canReset: currentStep.value > 0
}), [currentStep, isPlaying]);
// Methods that coordinate multiple optics
const play = () => setIsPlaying(true);
const pause = () => setIsPlaying(false);
const reset = () => {
setCurrentStep(0);
setIsPlaying(false);
};
return { ...sequence, play, pause, reset };
};
Pattern 5: The Deriver - Complex State Calculations
Create optics that perform complex calculations derived from multiple sources.
const useGamePhysics = () => {
const { position: playerPos } = usePlayerPosition();
const { velocity } = usePlayerVelocity();
const { gravity } = useWorldSettings();
const physics = useOptic(() => {
const newX = playerPos.value.x + velocity.value.x;
const newY = playerPos.value.y + velocity.value.y + gravity.value;
return {
position: { x: newX, y: newY },
speed: Math.sqrt(velocity.value.x ** 2 + velocity.value.y ** 2),
isFalling: velocity.value.y > 0,
isJumping: velocity.value.y < 0
};
}, [playerPos, velocity, gravity]);
return physics;
};
Real-World Example: Smart Parallax System
Let's build a complete parallax system using optical composition:
const useParallaxLayers = (layers = []) => {
const { position: mousePos } = useMousePosition();
const { position: scrollPos } = useScrollPosition();
const parallax = useOptic(() => {
const baseX = mousePos.value.x / window.innerWidth;
const baseY = (mousePos.value.y + scrollPos.value.y) / window.innerHeight;
return layers.map((layer, index) => ({
...layer,
transform: `translate3d(
${baseX * layer.depth * 50}px,
${baseY * layer.depth * 50}px,
0
)`,
opacity: 1 - (scrollPos.value.y / 1000) * layer.depth,
zIndex: layers.length - index
}));
}, [mousePos, scrollPos]);
return { layers: parallax };
};
This single optic combines mouse position, scroll position, and layer configuration to create a smooth parallax effect that updates automatically from all inputs!
Advanced Pattern: Optical Middleware
Create reusable transformation patterns that can be applied to any optic.
const withDebounce = (optic, delay) => {
const [debouncedValue, setDebouncedValue] = useRefraction(optic.value);
let timeout;
useFlash(() => {
clearTimeout(timeout);
timeout = setTimeout(() => {
setDebouncedValue(optic.value);
}, delay);
return () => clearTimeout(timeout);
}, [optic]);
return debouncedValue;
};
// Usage
const { position } = useMousePosition();
const debouncedPosition = withDebounce(position, 200);
Performance Optimization Patterns
Memoization for Expensive Calculations
const useExpensiveCalculation = (inputOptic) => {
const memoized = useOptic(() => {
// Only recalculate when input actually changes
return expensiveCalculation(inputOptic.value);
}, [inputOptic]);
return memoized;
};
Lazy Evaluation for Optional Optics
const useOptionalData = (enabled) => {
const sourceData = useDataSource();
const optional = useOptic(() => {
if (!enabled.value) return null;
return transformData(sourceData.value);
}, [enabled, sourceData]);
return optional;
};
Testing Composed Optics
import { renderHook, act } from '@refract-framework/testing';
test('parallax layers update from mouse and scroll', () => {
const { result } = renderHook(() => useParallaxLayers([{ depth: 0.5 }]));
// Simulate mouse movement
act(() => {
mockMousePosition(100, 200);
});
// Simulate scrolling
act(() => {
mockScrollPosition(0, 300);
});
expect(result.current.layers.value[0].transform).toContain('50px');
});
Common Composition Pitfalls
❌ Over-Composition
// Don't create optics for everything!
const [isOpen, setIsOpen] = useRefraction(false);
const [isVisible, setIsVisible] = useRefraction(false);
const [isActive, setIsActive] = useRefraction(false);
// ✅ Better: Combine related state
const uiState = useOptic(() => ({
open: isOpen.value,
visible: isVisible.value,
active: isActive.value
}), [isOpen, isVisible, isActive]);
❌ Circular Dependencies
// This will cause infinite loops!
const opticA = useOptic(() => opticB.value * 2, [opticB]);
const opticB = useOptic(() => opticA.value / 2, [opticA]);
❌ Over-Nesting
// Hard to debug and maintain
const superOptic = useOptic(() => ({
a: opticA.value,
b: opticB.value,
c: opticC.value,
nested: {
d: opticD.value,
e: opticE.value,
deeper: {
f: opticF.value
}
}
}), [/* 6+ dependencies */]);
Next Steps
Ready to master optical composition? Dive deeper with:
- Performance Optimization - Optimize complex compositions
- TypeScript Support - Add type safety to compositions
- Plugin Development - Create reusable composition patterns
- Advanced Effects - Combine composition with side effects
Join the Composition Revolution!
Share your amazing composition patterns with the community:
Remember: Great composition isn't about complexity - it's about creating clear, maintainable relationships between your optics. Now go compose something beautiful!