Skip to content

Feature Request: Add useTexture hook for async texture loading with states #12

Description

@jonobr1

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:
    • UseTextureOptions
    • TextureState
    • Type for supported source types
  • Implement basic hook structure:
    • useState for texture, loading, loaded, error states
    • useRef to 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 useEffect for 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 retryCount option
    • Exponential backoff for retries (optional)
  • Handle edge cases:
    • Invalid URLs
    • Network failures
    • CORS issues
    • Invalid image formats
  • Add error callbacks:
    • Call onError option on failures

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 cache option (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:
    • onLoad callback when texture loads
    • onError callback on failure
    • Pass texture instance to callbacks
  • Handle source changes:
    • Detect when src prop changes
    • Abort previous load if in progress
    • Start new load with new source
    • Reset states appropriately
  • 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 useTexture from lib/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 useTexture section 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

  1. Hook-based: Better for async operations than component
  2. State exposure: Loading/error states for UI feedback
  3. Caching built-in: Avoids redundant network requests
  4. Retry mechanism: Common pattern for network failures
  5. Flexible sources: Supports URLs, elements, canvas, video
  6. Callback support: Integration with external loading systems
  7. 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

  • useTexture hook 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions