1

I am trying to use suspense to load a GLTF model from Firebase storage. To do so I need to first get the URL for the model asynchronously using a getDownloadURL method before I can load it. What I am seeing is that the loader is repeatedly getting called but the response is never used - I am sure I've missed something simple..

I've put the code into this Code Sandbox it uses examples that seem fairly common on the internet, I've replaced the firebase accessor (as it would need private access keys to work as intended) but the replacement function is fairly trivial returning a url after a timeout.

To summarise the sandbox, the heart if it is a function to get the download url wrapped in a suspend function:

function getModelData(path) {
    const storage = firebase.storage();
    const urlPromise = storage.ref(path).getDownloadURL();

    return { url: suspend(urlPromise) };
}

This is used in my code like this:

export default function Model(props) {
    const modelData = getModelData(props.path);
    const gltf = useGLTF(modelData.url.read());

    return (
        <mesh rotation={props.rotation} position={props.position} scale={props.scale}>
            <primitive object={gltf.scene.clone(true)} dispose={null}/>
        </mesh>
    );
}

The suspend function correctly throws its promise, and the promise resolves setting result, but the suspend function itself is continuously called and the result method is always undefined.

2 Answers2

1

OK, so I continued to investigate and came to the conclusion that this was not going to work - the problem I have is that I am continually recreating the wrapper rather than creating it once and letting the promise complete and change the suspender status. I tried switching to setting a state in a useEffect hook and only loading the gltf if the state was set but that gave an error about an inconsistent number of hooks on render.

Instead I am using a state to set the model URL and putting the rendered model into a second class:

function ModelFromUrl(props) {
    const gltf = useGLTF(props.url);

    return (
        <mesh rotation={props.rotation || [0, 0, 0]} position={props.position || [0, 0, 0]} scale={props.scale || [1, 1, 1]}>
            <primitive object={gltf.scene.clone(true)} dispose={null}/>
        </mesh>
    );
}
export default function Model(props) {
    const [url, setUrl] = useState();

    useEffect(() => {
        firebase.storage().ref(props.path).getDownloadURL().then(url => setUrl(url));
    }, [props.path]);
    if (! url) {
        return null;
    }
    return <ModelFromUrl {...props} url={url}/>
}

It would have been nice to get the suspense mechanism working but it seems like this pattern will have to suffice.

0

The getDownloadURL is asyncrhonous so you would need to write your functions like this:

async function getModelData(path){
    const storage = firebase.storage();
    const urlPromise = await storage.ref(path).getDownloadURL();

    return { url: suspend(urlPromise) };
}

Tarik Huber
  • 7,061
  • 2
  • 12
  • 18
  • Thanks for taking the time to answer - I think that the point of this pattern is that the promise returned by getModelData is passed to the suspend wrapper so that it can be waited using its then method with suspended promise thrown back up the stack. I don't believe that making it async and awaiting it helps here. – Alex Whittaker May 30 '21 at 06:08
  • Yes but you don't return it as a Promise. That is the problem. You return there an object that has somwhere inside a promise. – Tarik Huber May 30 '21 at 21:27