Repository navigation
Feature Request: Add useTexture hook for async texture loading with states #12
Description
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
): TextureStateUsage 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)
- Create
lib/hooks/useTexture.ts - Define TypeScript interfaces:
UseTextureOptionsTextureState- Type for supported source types
- Implement basic hook structure:
useStatefor texture, loading, loaded, error statesuseRefto store texture instance- Return object with state values
- Export hook from file
Deliverable: Hook skeleton with TypeScript types
Phase 2: Texture Loading Logic
Files: lib/hooks/useTexture.ts
- Implement
useEffectfor texture loading:- Create Two.Texture instance with source
- Set loading state to true
- Handle Two.Texture callback for load completion
- Update loaded state when complete
- Store texture in ref and state
- Handle different source types:
- String URLs
- HTMLImageElement
- HTMLCanvasElement
- HTMLVideoElement
- Null/undefined sources
- Implement cleanup on unmount:
- Clear texture reference
- Abort pending loads if possible
Deliverable: Working texture loading with callback handling
Phase 3: Error Handling & Retry
Files: lib/hooks/useTexture.ts
- Add error state management:
- Detect load failures
- Set error state with Error object
- Clear loading state on error
- Implement retry mechanism:
retry()function to retry loading- Track retry attempts
- Respect
retryCountoption - Exponential backoff for retries (optional)
- Handle edge cases:
- Invalid URLs
- Network failures
- CORS issues
- Invalid image formats
- Add error callbacks:
- Call
onErroroption on failures
- Call
Deliverable: Robust error handling with retry capability
Phase 4: Caching System
Files: lib/hooks/useTexture.ts, lib/hooks/textureCache.ts (new)
- Create texture cache module:
- Map of URL → Texture instances
- Cache hit/miss tracking
- Memory management (LRU or size-based)
- Implement cache integration in hook:
- Check cache before creating new texture
- Store loaded textures in cache
- Return cached texture immediately (no loading state)
- Respect
cacheoption (default: true)
- Handle cache invalidation:
- Clear cache on unmount (if no other refs)
- Provide manual cache clear utility
- Handle src changes properly
- Add cache statistics (optional):
- Hit rate
- Total cached textures
- Memory usage
Deliverable: Texture caching to avoid redundant loads
Phase 5: Advanced Features
Files: lib/hooks/useTexture.ts
- Implement callback options:
onLoadcallback when texture loadsonErrorcallback on failure- Pass texture instance to callbacks
- Handle source changes:
- Detect when
srcprop changes - Abort previous load if in progress
- Start new load with new source
- Reset states appropriately
- Detect when
- Add preload optimization:
- Start loading immediately (don't wait for render)
- Support preloading without rendering
- Performance optimizations:
- Debounce rapid source changes
- Batch texture loads (future)
Deliverable: Full-featured hook with callbacks and optimizations
Phase 6: Integration & Export
Files: lib/main.ts, lib/hooks/index.ts
- Export
useTexturefromlib/hooks/index.ts - Export from
lib/main.ts - Export TypeScript interfaces
- Export cache utilities (optional):
clearTextureCache()getTextureCacheStats()
- Verify tree-shaking works
- Update package exports
Deliverable: Hook available in public API
Phase 7: Testing
Files: lib/hooks/__tests__/useTexture.test.tsx (new file)
- Test basic texture loading:
- Loading state transitions
- Loaded state when complete
- Texture instance created correctly
- Test error handling:
- Error state on failure
- Error callback invoked
- Retry mechanism works
- Test caching:
- Cache hits return immediately
- Cache misses load texture
- Multiple hooks share cached texture
- Test source changes:
- New texture loads when src changes
- Previous load aborted/cleaned up
- States reset properly
- Test callbacks:
- onLoad called with texture
- onError called with error
- Test cleanup:
- Resources freed on unmount
- No memory leaks
- Mock Two.Texture for isolated testing
- Test with different source types
Deliverable: Comprehensive test coverage
Phase 8: Documentation & Examples
Files: README.md, src/App.tsx, documentation site
- Add
useTexturesection to main README - Document all options and return values
- Create interactive examples:
- Basic texture loading with spinner
- Error handling with retry
- Multiple texture preloading
- Progress indicator
- Cached texture reuse
- Dynamic texture switching
- Document caching behavior
- Add troubleshooting section:
- CORS issues
- Image format support
- Memory management
- Include performance best practices
- TypeScript usage examples
- Compare with Texture component
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 loadedPros: Future-proof, idiomatic React
Cons: Requires Suspense support, experimental
Decision: Start without Suspense, add later
Resources
- Two.js Texture Documentation
- Two.js Sprite Documentation
- Two.js Image Documentation
- MDN - HTMLImageElement
- MDN - Loading Images
Success Criteria
-
useTexturehook successfully loads textures from URLs - Loading, loaded, and error states work correctly
- Error handling and retry mechanism function properly
- Texture caching prevents redundant loads
- Multiple components can share cached textures
- Dynamic source changes handled correctly
- Callbacks (onLoad, onError) invoked appropriately
- Works with string URLs and HTMLElement sources
- Cleanup on unmount prevents memory leaks
- Full TypeScript support with proper types
- Comprehensive tests with good coverage
- Documentation includes examples and API reference
- Performance equivalent to direct Two.Texture usage
- Cache management is memory-efficient
- No breaking changes to existing Texture component
Labels: enhancement, feature request, hooks, textures
Milestone: v0.3.0
Activity
jonobr1 commentedon Sep 3, 2026
Reopening after the parity audit: the current
mainbranch exportsTexture, but it does not contain or export auseTexturehook, so the original request is not presently complete.Suggested delivery order:
Coordinate ownership and cleanup with #29, and resource composition / documentation with #31. Please include Strict Mode and server-rendering behavior: constructing Texture, Image, or Sprite during render currently starts resource work and fails in a non-DOM server render.
jonobr1 commentedon Sep 3, 2026
Declarative Parity Update from #30 (#37)
Sprite.srcnow acceptsTwo.Texture | stringdirectly, andTexturePropsis now exported in the public API. OnceuseTextureis implemented, the loadedTwo.Textureinstance can be passed directly to<Sprite src={texture} />or<Image texture={texture} />without type assertions or imperative workarounds.