Skip to main content

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 };
};
When to Use Mapping
  • 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 };
};
Performance Considerations

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 };
};
Composition in Action

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:

  1. Performance Optimization - Optimize complex compositions
  2. TypeScript Support - Add type safety to compositions
  3. Plugin Development - Create reusable composition patterns
  4. 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!