April 10, 2020 Alex de Boutray 6 min read

I love using React for multiple reasons. I like the writing style, I like the tooling, I like the ecosystem.

But sometimes you run into a library that doesn't have a React adapter. It can either be pretty old, kind of niche, closed source, etc.

In this series of articles, we’ll look into how to convert an existing imperative JS library into a declarative React component.

We’ll use the Sketchfab Viewer as an example. Sketchfab develops an amazing 3d suite that includes a very configurable web viewer for use with their models.

You can see that their doc is pretty extensive, if a bit messy. It includes a lot of examples and it took me a long time to run into a use case that was not already covered. Yet you can also see that they use an imperative style of JS that doesn’t fit well into the React patterns.

api . addEventListener ( "viewerready" , function ( ) { console . log ( "Viewer is ready" ) ; } ) ;

We are going to build a React function component on top of that library. It will accept a config object to declaratively change a color on the model. It will also expose a callback to react to a click event on a part of the model. And we'll use hooks because they're fun!

You can find the final code for this part here.

Part 1: Initializing and exposing the api object

Part 2: Working on it!

Initializing and exposing the api object

Throughout this project we'll work with this fancy chair, courtesy of the official Sketchfab examples.

Start the project with create-react-app

First things first, let's generate a basic CRA project:

npx create-react-app sketchfab-react

In src/ , add a Viewer.jsx file and create an empty component inside.

import React from "react" ; export const Viewer = ( ) => < > </ > ;

Add that component inside App.jsx

import React from "react" ; import "./App.css" ; import { Viewer } from "./Viewer" ; function App ( ) { return ( < div className = " App " > < Viewer /> </ div > ) ; } export default App ;

Now let's import the library. Sketchfab provides a package install through npm but that's not always the case for other libraries, so we'll include the source in the head of public/index.html .

< script type = " text/javascript " src = " https://static.sketchfab.com/api/sketchfab-viewer-1.7.1.js " > </ script >

This import will make window.Sketchfab available globally. If you're using Typescript, you will need to ignore it.

Render the Sketchfab Viewer in an iframe

According to the docs, we need to create an iframe and give it to the Sketchfab constructor to render the Viewer.

The way to do this in React is, in Viewer.jsx:

import React , { useEffect , useRef } from "react" ; const MODEL_UID = "c632823b6c204797bd9b95dbd9f53a06" ; export const Viewer = ( ) => { const viewerIframeRef = useRef ( null ) ; const ViewerIframe = ( < iframe ref = { viewerIframeRef } title = "sketchfab-viewer" style = { { height : 400 , width : 600 } } / > ) ; useEffect ( ( ) => { let client = new window . Sketchfab ( viewerIframeRef . current ) ; client . init ( MODEL_UID , { success : ( ) => { } , error : ( ) => { console . log ( "Viewer error" ) ; } , } ) ; } , [ ] ) ; return ViewerIframe ; } ;

And you should have the viewer displaying!

Expose the api object to access it imperatively

One of the great things about the Viewer API is that it allows us to transform our model from the browser. Here, we'll try to make the chair red.

First we need to extract the api object and store it. We could put it in the state of our Viewer component, but we'll want to use it higher up in the tree, so we will pass a ref as prop and give it the api.

export const Viewer = ( { apiRef } ) => { success : api => { apiRef . current = api ; } , } ;

Now, let's get it inside App.

function App ( ) { const apiRef = useRef ( null ) ; return ( < div className = " App " > < Viewer apiRef = { apiRef } /> </ div > ) ; }

To check that it works, let's try to change the color of the background. There's a method for that!

Let's add this code to App.jsx :

function App ( ) { const changeBackgroundColor = ( ) => { apiRef . current . setBackground ( { color : [ Math . random ( ) , Math . random ( ) , Math . random ( ) , 1 ] , } ) ; } ; return ( < div className = " App " > < button onClick = { changeBackgroundColor } > Change background color </ button > < Viewer apiRef = { apiRef } /> </ div > ) ; }

Save, click on the button, voilà!

If it doesn't work, check that you have clicked on the play button of the viewer beforehand to load the model ;)

Change the chair color

Alright, that function is a bit bigger but you'll have to trust me.

function App ( ) { const changeChairColor = ( ) => { apiRef . current . getMaterialList ( ( err , materials ) => { const plasticMaterial = materials . find ( ( material ) => material . name === "Blue plastic" ) ; plasticMaterial . channels . AlbedoPBR . color = [ Math . random ( ) , Math . random ( ) , Math . random ( ) , ] ; apiRef . current . setMaterial ( plasticMaterial , ( ) => { console . log ( "Updated chair color" ) ; } ) ; } ) ; } ; return ( < div className = " App " > < button onClick = { changeBackgroundColor } > Change background color </ button > < button onClick = { changeChairColor } > Change chair color </ button > < Viewer apiRef = { apiRef } /> </ div > ) ; }

A new color for our chair!

Hooks refactor

To abstract the initialization code and clean up our component, we'll write a custom hook that leverages all the code previously written plus a useState for the api object.

Custom hooks are one of the most powerful tools to keep our code well organized. Once you start using them, there's no going back.

import React , { useEffect , useRef , useState } from "react" ; const MODEL_UID = "c632823b6c204797bd9b95dbd9f53a06" ; const useSketchfabViewer = ( ) => { const viewerIframeRef = useRef ( null ) ; const [ api , setApi ] = useState ( ) ; const ViewerIframe = ( < iframe ref = { viewerIframeRef } title = "sketchfab-viewer" style = { { height : 400 , width : 600 } } / > ) ; useEffect ( ( ) => { let client = new window . Sketchfab ( viewerIframeRef . current ) ; client . init ( MODEL_UID , { success : setApi , error : ( ) => { console . log ( "Viewer error" ) ; } , } ) ; } , [ ] ) ; return [ ViewerIframe , api ] ; } ; export const Viewer = ( { apiRef } ) => { const [ ViewerIframe , api ] = useSketchfabViewer ( ) ; apiRef . current = api ; return ViewerIframe ; } ;

What we learned

We can use refs to feed DOM elements to libraries that need them.

to feed to libraries that need them. We can use refs to give back objects to parents . Another solution could have been to use a React Context.

. Another solution could have been to use a React Context. We can use custom hooks to make our code cleaner and more readable.

You can find the final source code here.

Shout-out to Sketchfab for their amazing product and in particular to Arthur Jamain for the great collaboration we had over the last few months.