Summary
Add a useTexture React hook to provide declarative texture loading with loading/error states and lifecycle management. While a <Texture> component exists, a hook would enable better async handling, preloading, caching, and integration with loading UI patterns.
Motivation
Two.js's Texture class loads images asynchronously via callbacks. The current <Texture> component is a thin wrapper that doesn't expose loading states or errors. A dedicated hook would:
- Provide loading/loaded/error states for UI feedback
- Enable texture preloading before rendering
- Support texture caching to avoid re-loading
- Handle cleanup automatically
- Integrate with React Suspense patterns (future)
- Enable multiple texture loading patterns
Two.js Texture Background
How Two.js Texture works:
const texture = new Two.Texture(src, callback);
// Properties:
texture.loaded; // Boolean indicating load state
texture.image; // HTMLImageElement
texture.src; // Source URL
// Callback invoked when loaded:
new Two.Texture('/image.jpg', (texture) => {
console.log('Loaded!', texture);
});
Common use cases:
- Loading images for Sprite components
- Texture mapping for Image components
- Creating texture atlases
- Loading sprite sheets for ImageSequence
Proposed API
Hook Signature
interface UseTextureOptions {
onLoad?: (texture: RefTexture) => void;
onError?: (error: Error) => void;
retryCount?: number; // Auto-retry on failure
cache?: boolean; // Cache loaded textures (default: true)
}
interface TextureState {
texture: RefTexture | null;
loading: boolean;
loaded: boolean;
error: Error | null;
retry: () => void;
}
function useTexture(
src: string | HTMLImageElement | HTMLCanvasElement | HTMLVideoElement | null,
options?: UseTextureOptions
): TextureState
Usage Examples
Basic Texture Loading
import { Canvas, Sprite, useTexture } from 'react-two.js';
function ImageSprite() {
const { texture, loading, error } = useTexture('/images/character.png');
if (loading) return <Text value="Loading..." />;
if (error) return <Text value={`Error: ${error.message}`} />;
if (!texture) return null;
return <Sprite texture={texture} x={100} y={100} />;
}
With Loading UI
function LoadingImage() {
const { texture, loading, loaded } = useTexture('/images/background.jpg');
return (
<>
{loading && (
<Group>
<Circle radius={20} stroke="gray" linewidth={2} />
<Text value="Loading..." y={30} />
</Group>
)}
{loaded && texture && (
<Image texture={texture} x={200} y={200} />
)}
</>
);
}
Error Handling with Retry
function RobustImage({ src }: { src: string }) {
const { texture, loading, error, retry } = useTexture(src, {
retryCount: 3,
onError: (err) => console.error('Failed to load:', err)
});
if (loading) return <Text value="Loading..." />;
if (error) {
return (
<Group>
<Text value="Failed to load image" />
<Rectangle
y={30}
width={100}
height={30}
fill="blue"
onClick={() => retry()}
/>
<Text value="Retry" y={30} />
</Group>
);
}
return <Sprite texture={texture!} />;
}
Preloading Multiple Textures
function MultiTextureLoader() {
const char1 = useTexture('/sprites/char1.png');
const char2 = useTexture('/sprites/char2.png');
const bg = useTexture('/sprites/background.png');
const allLoaded = char1.loaded && char2.loaded && bg.loaded;
const progress = [char1, char2, bg].filter(t => t.loaded).length / 3;
if (!allLoaded) {
return (
<Group>
<Text value={`Loading: ${Math.round(progress * 100)}%`} />
<Rectangle
width={200 * progress}
height={20}
fill="green"
/>
</Group>
);
}
return (
<>
<Image texture={bg.texture!} />
<Sprite texture={char1.texture!} x={100} />
<Sprite texture={char2.texture!} x={200} />
</>
);
}
Cached Texture Reuse
// First component loads texture
function Component1() {
const { texture } = useTexture('/shared-texture.png');
return <Sprite texture={texture} x={0} />;
}
// Second component reuses cached texture (no re-download)
function Component2() {
const { texture, loading } = useTexture('/shared-texture.png');
// loading will be false if already cached
return <Sprite texture={texture} x={100} />;
}
Dynamic Texture Switching
function DynamicSprite({ character }: { character: string }) {
const src = `/characters/${character}.png`;
const { texture, loading } = useTexture(src);
// When character prop changes, hook automatically loads new texture
return (
<>
{loading && <Circle radius={10} fill="gray" />}
{texture && <Sprite texture={texture} x={150} y={150} />}
</>
);
}
Canvas/Video Sources
function CanvasTexture() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const { texture } = useTexture(canvasRef.current);
useEffect(() => {
// Draw something on canvas
const ctx = canvasRef.current?.getContext('2d');
if (ctx) {
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, 100, 100);
}
}, []);
return (
<>
<canvas ref={canvasRef} width={100} height={100} style={{ display: 'none' }} />
{texture && <Sprite texture={texture} />}
</>
);
}
With Callbacks
function CallbackExample() {
const { texture } = useTexture('/image.png', {
onLoad: (tex) => {
console.log('Loaded:', tex.image.width, 'x', tex.image.height);
},
onError: (err) => {
console.error('Load failed:', err);
}
});
return texture ? <Sprite texture={texture} /> : null;
}
Implementation Phases
Phase 1: Core Hook Structure
Files: lib/hooks/useTexture.ts (new file)
Deliverable: Hook skeleton with TypeScript types
Phase 2: Texture Loading Logic
Files: lib/hooks/useTexture.ts
Deliverable: Working texture loading with callback handling
Phase 3: Error Handling & Retry
Files: lib/hooks/useTexture.ts
Deliverable: Robust error handling with retry capability
Phase 4: Caching System
Files: lib/hooks/useTexture.ts, lib/hooks/textureCache.ts (new)
Deliverable: Texture caching to avoid redundant loads
Phase 5: Advanced Features
Files: lib/hooks/useTexture.ts
Deliverable: Full-featured hook with callbacks and optimizations
Phase 6: Integration & Export
Files: lib/main.ts, lib/hooks/index.ts
Deliverable: Hook available in public API
Phase 7: Testing
Files: lib/hooks/__tests__/useTexture.test.tsx (new file)
Deliverable: Comprehensive test coverage
Phase 8: Documentation & Examples
Files: README.md, src/App.tsx, documentation site
Deliverable: Complete documentation with examples
Technical Considerations
Two.js Texture Implementation
class Texture {
constructor(src, callback) {
this.src = src;
this.loaded = false;
if (src instanceof Image) {
this.image = src;
this.loaded = true;
callback?.(this);
} else {
this.image = new Image();
this.image.onload = () => {
this.loaded = true;
callback?.(this);
};
this.image.onerror = (e) => {
// Error handling
};
this.image.src = src;
}
}
}
Design Decisions
- Hook-based: Better for async operations than component
- State exposure: Loading/error states for UI feedback
- Caching built-in: Avoids redundant network requests
- Retry mechanism: Common pattern for network failures
- Flexible sources: Supports URLs, elements, canvas, video
- Callback support: Integration with external loading systems
- Memory-conscious: Automatic cleanup and cache management
Caching Strategy
Cache key: Use src string as key
- Simple and effective for URLs
- Canvas/video elements: Use object reference or unique ID
- LRU (Least Recently Used) eviction when cache full
- Configurable cache size limit
Cache invalidation:
- Remove from cache when no components reference it
- Manual clear function for development
- Weak references to avoid memory leaks
Performance Considerations
- Textures loaded asynchronously don't block rendering
- Cache prevents redundant network requests
- Multiple components can share same texture instance
- Consider lazy loading for off-screen images
- Image size affects memory usage (compress images)
Error Handling
Common errors:
- 404 Not Found: Wrong URL or missing file
- CORS: Cross-origin restrictions
- Invalid format: Unsupported image format
- Network failure: Timeout, offline, etc.
Handle gracefully:
- Clear error messages
- Retry with backoff
- Fallback textures (future)
- User feedback
Relationship with Texture Component
Texture Component:
- Declarative JSX syntax
- No loading states exposed
- Simple use cases
useTexture Hook:
- More control over loading
- Loading/error state management
- Caching and preloading
- Better for complex scenarios
Recommendation: Use hook for most cases, component for simple inline textures
Alternative Approaches Considered
1. Enhance Texture Component with Render Props:
<Texture src="/image.png">
{({ texture, loading }) => loading ? <Loading /> : <Sprite texture={texture} />}
</Texture>
Pros: Component-based, declarative
Cons: Verbose, less flexible than hook
Decision: Hook is more versatile
2. Global Texture Manager:
const textureManager = useTextureManager();
const tex = textureManager.load('/image.png');
Pros: Centralized management
Cons: Global state, complex API
Decision: Per-component hooks are simpler
3. React Suspense Integration:
const texture = useTexture('/image.png', { suspense: true });
// Throws promise until loaded
Pros: Future-proof, idiomatic React
Cons: Requires Suspense support, experimental
Decision: Start without Suspense, add later
Resources
Success Criteria
Labels: enhancement, feature request, hooks, textures
Milestone: v0.3.0
Summary
Add a
useTextureReact hook to provide declarative texture loading with loading/error states and lifecycle management. While a<Texture>component exists, a hook would enable better async handling, preloading, caching, and integration with loading UI patterns.Motivation
Two.js's
Textureclass loads images asynchronously via callbacks. The current<Texture>component is a thin wrapper that doesn't expose loading states or errors. A dedicated hook would:Two.js Texture Background
How Two.js Texture works:
Common use cases:
Proposed API
Hook Signature
Usage Examples
Basic Texture Loading
With Loading UI
Error Handling with Retry
Preloading Multiple Textures
Cached Texture Reuse
Dynamic Texture Switching
Canvas/Video Sources
With Callbacks
Implementation Phases
Phase 1: Core Hook Structure
Files:
lib/hooks/useTexture.ts(new file)lib/hooks/useTexture.tsUseTextureOptionsTextureStateuseStatefor texture, loading, loaded, error statesuseRefto store texture instanceDeliverable: Hook skeleton with TypeScript types
Phase 2: Texture Loading Logic
Files:
lib/hooks/useTexture.tsuseEffectfor texture loading:Deliverable: Working texture loading with callback handling
Phase 3: Error Handling & Retry
Files:
lib/hooks/useTexture.tsretry()function to retry loadingretryCountoptiononErroroption on failuresDeliverable: Robust error handling with retry capability
Phase 4: Caching System
Files:
lib/hooks/useTexture.ts,lib/hooks/textureCache.ts(new)cacheoption (default: true)Deliverable: Texture caching to avoid redundant loads
Phase 5: Advanced Features
Files:
lib/hooks/useTexture.tsonLoadcallback when texture loadsonErrorcallback on failuresrcprop changesDeliverable: Full-featured hook with callbacks and optimizations
Phase 6: Integration & Export
Files:
lib/main.ts,lib/hooks/index.tsuseTexturefromlib/hooks/index.tslib/main.tsclearTextureCache()getTextureCacheStats()Deliverable: Hook available in public API
Phase 7: Testing
Files:
lib/hooks/__tests__/useTexture.test.tsx(new file)Deliverable: Comprehensive test coverage
Phase 8: Documentation & Examples
Files:
README.md,src/App.tsx, documentation siteuseTexturesection to main READMEDeliverable: Complete documentation with examples
Technical Considerations
Two.js Texture Implementation
Design Decisions
Caching Strategy
Cache key: Use
srcstring as keyCache invalidation:
Performance Considerations
Error Handling
Common errors:
Handle gracefully:
Relationship with Texture Component
Texture Component:
useTexture Hook:
Recommendation: Use hook for most cases, component for simple inline textures
Alternative Approaches Considered
1. Enhance Texture Component with Render Props:
Pros: Component-based, declarative
Cons: Verbose, less flexible than hook
Decision: Hook is more versatile
2. Global Texture Manager:
Pros: Centralized management
Cons: Global state, complex API
Decision: Per-component hooks are simpler
3. React Suspense Integration:
Pros: Future-proof, idiomatic React
Cons: Requires Suspense support, experimental
Decision: Start without Suspense, add later
Resources
Success Criteria
useTexturehook successfully loads textures from URLsLabels: enhancement, feature request, hooks, textures
Milestone: v0.3.0