DEV Community

Yosuke Sakurai
Yosuke Sakurai

Posted on

How I Built a 3D Vision Board in React Native Using Three.js

The Problem

I was building a manifestation app in React Native and needed a vision board feature. Not a static collage. An immersive 3D space users could actually step into.

The challenge: Three.js is built for the web, not React Native. I needed a bridge that did not destroy performance or bundle size.

## The Architecture

I chose a WebView-based approach rather than a native Three.js port.

  • React Native handles the shell, navigation, auth, and Firebase sync
  • WebView renders the 3D scene using Three.js
  • PostMessage API handles communication between RN and the WebView
  • Firebase Storage serves optimized images for the vision board

Why not react-three-fiber? At the time of building, R3F's React Native support was experimental and the bundle size exceeded 15MB. A WebView let me lazy-load the 3D scene only when
needed.

## The 3D Scene

The scene is a simple cylindrical panorama. Users upload images, which are mapped to floating planes in 3D space. An affirmation text overlay follows the camera.

Here is the basic Three.js setup:


javascript
  // Inside the WebView
  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
  const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });

  // Ambient lighting
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
  scene.add(ambientLight);

  // Floating image planes
  const geometry = new THREE.PlaneGeometry(3, 2);
  const texture = new THREE.TextureLoader().load(imageUrl);
  const material = new THREE.MeshBasicMaterial({ map: texture });
  const plane = new THREE.Mesh(geometry, material);
  plane.position.set(x, y, z);
  scene.add(plane);

  Performance Lessons

  1. Lazy load everything
  The WebView is mounted only when the user opens the vision board. The initial bundle does not include Three.js.

  2. Image optimization
  Users upload high-res photos. I compress them to 1024px width on the client before upload using react-native-image-resizer. This cut load times from 4 seconds to under 1 second.

  3. Limit the scene complexity
  I cap the board at 12 images. More than that and frame rate drops below 50fps on mid-range Android devices.

  4. Handle WebView memory
  On Android, WebViews are notorious for memory leaks. I explicitly destroy the WebView instance when the user navigates away:

  useEffect(() => {
    return () => {
      if (webViewRef.current) {
        webViewRef.current.reload(); // forces cleanup
      }
    };
  }, []);

  The Communication Bridge

  The React Native shell sends commands to the WebView via URL hashes, and the WebView sends events back via window.ReactNativeWebView.postMessage.

  // React Native sends a command
  const sendCommand = (command) => {
    webViewRef.current?.injectJavaScript(`
      window.dispatchEvent(new MessageEvent('message', {
        data: ${JSON.stringify(command)}
      }));
      true;
    `);
  };

  // WebView sends back an event
  const handleMessage = (event) => {
    const data = JSON.parse(event.nativeEvent.data);
    if (data.type === 'planeClicked') {
      // Handle image selection
    }
  };

  The Unlock-Screen Mechanic

  The real insight was not the 3D board. It was the affirmation delivery mechanism.

  Instead of push notifications (70% swipe-away rate), the app shows an affirmation every time the user unlocks their phone. This piggybacks on an existing habit rather than creating a new
  one.

  Technically, this uses React Native's AppState API. When the app transitions from background to active, we render a full-screen modal with the affirmation. The user must hold for 3 seconds
   while feeling the emotion before they can dismiss it.

  Results

  - Day 7 retention: 34%
  - Day 30 retention: 18%
  - Average daily unlock engagements: 23
  - Bundle size: 12MB (without the lazy-loaded WebView assets)

  What I Would Do Differently

  1. Use Expo Modules sooner. I ejected from Expo too early and spent weeks fixing native module issues.
  2. Test on mid-range Android first. I developed on an iPhone 15 Pro and was shocked by Android performance.
  3. Start with a simpler 3D scene. The first version had particle effects and fog. It looked great on my device and crashed on a Samsung A51.

  Source

  The app is free on iOS and Android. If you are building with React Native and Three.js, feel free to reach out with questions.

  Have you tried mixing WebViews with native React Native? I would love to hear about your approach.
Enter fullscreen mode Exit fullscreen mode

Top comments (0)