jonobr1 / jonobr1/react-two.js
Feature Request: Add useTexture hook for async texture loading with states
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 15
- Forks
- 2
- Avg merge
- 7h 7m
- Merged PRs (30d)
- 3
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
): 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)
- 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 loaded
Pros: 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
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the proposed phases for lib/hooks/useTexture.ts, then review the export points in lib/hooks/index.ts and lib/main.ts. Run or add coverage in lib/hooks/tests/useTexture.test.tsx for loading, errors, retries, caching, source changes, callbacks, and cleanup. Done means the hook is publicly exported, tested, and documented in the named README and example files.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react, typescript
- Domain
- frontend
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100