├── .gitignore
├── .prettierrc
├── README.md
├── package.json
├── public
├── 404.html
├── favicon.ico
└── index.html
├── src
├── App.js
├── ThreeJSManager
│ ├── Canvas.js
│ ├── ThreeJSManager.js
│ ├── index.js
│ ├── useAnimationFrame.js
│ └── useThree.js
├── example-asteroids
│ ├── Asteroid.js
│ ├── GameExample.js
│ ├── LaserStrengthMeter.js
│ ├── LaserStrengthMeter.three.js
│ ├── Laserbeam.js
│ ├── Spaceship.js
│ ├── hooks
│ │ ├── useAsteroidsGame
│ │ │ ├── index.js
│ │ │ ├── useAsteroidsGame.js
│ │ │ └── useAsteroidsGameUtil.js
│ │ ├── useKnobs.js
│ │ └── useSpaceshipControl.js
│ └── threeSetup.js
├── example-cube
│ ├── CameraControls.js
│ ├── Cube.js
│ ├── CubeExample.js
│ ├── Grid.js
│ └── threeSetup.js
├── example-globe
│ ├── CameraControls.js
│ ├── Country.js
│ ├── GlobeContainer.js
│ ├── MapContainer.js
│ ├── MapExample.js
│ ├── countries.geo.json
│ ├── hooks
│ │ └── useWorldMap.js
│ ├── mapUtils.js
│ └── threeSetup.js
├── index.css
└── index.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "all",
3 | "singleQuote": true
4 | }
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Hooks + Three.js
2 |
3 | ## Motivation/Background
4 | With three.js, you need to instantiate several objects each time you use it. You need to create a Scene, a camera, a renderer, some 3D objects, and maybe a canvas. Beyond that, if you need controls, maybe you setup [`DAT.gui`](https://github.com/dataarts/dat.gui) like the examples use.
5 |
6 | The renderer needs a reference to the camera and the Scene. You need a reference to the Scene to add any 3D objects you create. For camera controls you need reference to the canvas DOM element. It's up to you how to structure everything cleanly.
7 |
8 | In contrast, React devs are now used to the ease and simplicity of using `create-react-app` to try things out. It would be great to have a React component you could throw some random three.js code into and have it work!
9 |
10 | Taking the idea further, what if we could model three.js scene objects like React components and write code like this:
11 |
12 | ```html
13 |
14 |
15 |
16 |
17 | ```
18 |
19 | The pattern I explain below aims to allow this in a very minimally obtrusive way.
20 |
21 | ## What is the end result?
22 | - A component: `ThreeJSManager`
23 | - A custom hook: `useThree`
24 |
25 | Combining them allows you to create React-ish three.js components without losing any control of three.js
26 |
27 | ## API
28 |
29 | ### `ThreeJSManager`:
30 | This component takes 3 props:
31 | - `getCamera` _(Function)_: Function that returns a three.js `Camera`. Function called with a `canvas` element
32 | - `getRenderer` _(Function)_: Function that returns a three.js `Renderer`. Function called with a `canvas` element
33 | - `getScene` _(Function)_: Function that returns a three.js `Scene`.
34 |
35 | The output of these functions, along with a `canvas` and `timer`, are made available in a React `Context`. Any of the components you add the `useThree` hook to need to be a child of this component.
36 |
37 | ### `useThree` Custom Hook:
38 | ```
39 | useThree(setup, [destroy])
40 | ```
41 | `useThree` custom hook relies on the context provided by `ThreeJSManager`, so it can only be used in components that have `ThreeJSManager` as an ancestor.
42 |
43 | Arguments:
44 | - `setup` _(Function)_: This function will be called when the component mounts. It gets called with the context value provided by `ThreeJSManager`. This function is where you setup the 3D objects to use in your component. Whatever you return here will be available to you later as the output of the `getEntity` function, which is on the object returned by `useThree`.
45 | - `destroy` _(Function)_: Optional. This function will be called when the component is unmounted. It gets called with 2 arguments: the context values provided by `ThreeJSManager`, and a reference to whatever you returned from `setup`. If the `destroy` param isn't passed, `scene.remove` is called with the return value of `setup` by default. _**Note**: the return value of `setup` is the same as the output of `getEntity` described below_
46 |
47 | Returns: _(Object)_
48 |
49 | `useThree` returns an object which has all the values from the context provided by `ThreeJSManager`, and a `getEntity` function which returns a reference to the return value of the `setup` function. Whatever is returned from `setup` is stored internally inside the hook for you to access with `getEntity` whenever you need to, ie when props change.
50 |
51 | ## How does it work? React Hooks.
52 |
53 | There are a few features in React 16.x that make using three.js (or any external library) in a React app a lot cleaner. Those are [`forwardRef`](https://reactjs.org/docs/forwarding-refs.html), and some of the new, experimental [Hooks](https://reactjs.org/docs/hooks-intro.html): [`useRef`](https://reactjs.org/docs/hooks-reference.html#useref), and most importantly [`useEffect`](https://reactjs.org/docs/hooks-reference.html#useeffect).
54 |
55 | Read the docs above, but in a nutshell what the features allow is the ability to create function components which:
56 |
57 | - Use `useEffect` to configurably run arbitrary JS (such as calling a third party library)
58 | - Use `useRef` to store a reference to any arbitrary value (such as third-party classes)
59 | - Combine the above with `forwardRef` to create a component that makes any arbitrary reference available to it's parent
60 |
61 | Here is how you could use these hooks to make a `Cube` functional component:
62 |
63 | ```js
64 | function Cube = (props) {
65 | const entityRef = useRef()
66 | const scene = getScene()
67 |
68 | useEffect(
69 | () => {
70 | entityRef.current = createThreeJSCube(scene, props)
71 | return () => {
72 | cleanupThreeJSCube(scene, entityRef.current)
73 | }
74 | },
75 | [],
76 | )
77 |
78 | return null
79 | }
80 | ```
81 |
82 | Ignoring for now where the component gets `scene` from, what we've done is created a React component, which, when mounted, calls `createThreeJSCube` and stores a reference to the return value, and when unmounted, calls `cleanupThreeJSCube`. It renders `null`, so doesn't effect the DOM; it *only* has side effects. Interesting.
83 |
84 | In case you haven't read up on `useEffect` yet, the 2nd argument is the hook's dependencies, by specifying an empty array, we're indicating this hook doesn't have dependencies and should only be run once. Omitting the argument indicates it should be run on every render, and adding references into the array will cause the hook to run only when the references have changed.
85 |
86 | Using this knowledge, we can add a second hook to our `Cube` component to run some effects when props change. Since we stored the output from our three.js code into `entityRef.current`, we can now access it from this other hook:
87 |
88 | ```js
89 | function Cube(props) {
90 | …
91 | useEffect(
92 | () => {
93 | updateCubeWithProps(entityRef.current, props)
94 | },
95 | [props]
96 | )
97 | }
98 | ```
99 |
100 | We now have a React component which adds a 3D object to a three.js scene, alters the object when it gets new props, and cleans itself up when we unmount it. Awesome! Now we just need to make `scene` available in our component so that it actually works.
101 |
102 | ## `forwardRef`
103 |
104 | Before discussing how we get `scene` available in our component, let's discuss another newer React feature that will help us setup three.js in the way we need: `forwardRef`. Remember, before we can even get to adding 3D objects to the `scene`, we still need to setup our canvas, renderer, camera, and all of that.
105 |
106 | Consider the fact that, in three.js, several things need reference to the `canvas` element. In more vanilla usages this `canvas` is created by THREE code itself, but we want more control, so we're going to render it from a React component so we can encapsulate resize actions and anything else specific to the canvas in that component. Now we have a problem though, in that, the DOM element is only available in that component. How do we solve this? `forwardRef`! With `forwardRef`, we can create a `Canvas` component, that renders a canvas element, and forwards it's ref to it. So anyone for anyone rendering ``, `myRef` will point to the `canvas` HTML element itself, not the `Canvas` React component. Cool!
107 |
108 | ```js
109 | const Canvas = forwardRef((props, ref) => {
110 | …
111 | return (
112 |
113 | )
114 | })
115 | ```
116 |
117 | Also remember that, from the React docs, `ref`s are not just for DOM element references! We could set and forward a `ref` to any value.
118 |
119 | ## `ThreeJSManager` Component
120 |
121 | Using the above techniques, we can create a `ThreeJSManager` component that has `ref`s to everything we need to use three.js: we'll pass it functions that return the `camera`, `renderer`, `scene` objects, and we'll use our `Canvas` component to reference the `canvas` DOM element.
122 |
123 | However, we'll still need to make these objects available to child components. For this, we'll have `ThreeJSManager` render a `Context.Provider` with all these values. Most components will only need `scene`, but the `canvas` and `camera` object will be useful for components that render for example camera controls.
124 |
125 | Now with the context `Provider` setup, we can use the `useContext` hook to access the scene in our React/three.js components:
126 |
127 | ```js
128 | function Cube = (props) {
129 | const context = useContext(ThreeJSContext)
130 | const { scene } = context
131 | …
132 | }
133 | ```
134 |
135 | In a nutshell, `ThreeJSManager` abstracts away the base-level three.js tasks you need to do to before you can add 3D objects. Here's how its return value might look:
136 |
137 | ```html
138 |
145 | { props.children }
146 |
147 | ```
148 |
149 | And here's how we might use it in our app:
150 | ```html
151 |
156 |
157 |
158 |
159 |
160 |
161 | ```
162 |
163 | With `Ground`, `Lights`, `CameraControls`, and `Cube` being components that make use of the `useThree` hook.
164 |
165 | ## `useThree` Custom Hook
166 | Let's look at what `useThree` does:
167 |
168 | - Accesses the `scene` and other three.js objects with `useContext`
169 | - Initializes a placeholder that will store the 3D object with `useRef` (`entityRef`)
170 | - Runs code on mount that instantializes the 3D object, assigns it to `entityRef`, adds it to the `scene`, and returns a cleanup function that removes it from `scene` with `useEffect`
171 | - Returns an object with a `getEntity` function that can be used in other effects (such as when props change) to update the 3D object.
172 |
173 | Here's the code for our custom hook:
174 |
175 | ```js
176 | import { ThreeJSContext } from './ThreeJSManager';
177 |
178 | const useThree = (setup, destroy) => {
179 | const entityRef = useRef();
180 | const context = useContext(ThreeJSContext);
181 |
182 | const getEntity = () => entityRef.current;
183 |
184 | useEffect(
185 | () => {
186 | entityRef.current = setup(context);
187 |
188 | return () => {
189 | if (destroy) {
190 | return destroy(context, getEntity());
191 | }
192 | context.scene.remove(getEntity());
193 | };
194 | },
195 | [],
196 | );
197 |
198 | return {
199 | getEntity,
200 | ...context,
201 | };
202 | }
203 | ```
204 |
205 | Here's how you'd use it to add a simple grid object to the scene:
206 |
207 | ```js
208 | const Grid = () => {
209 | useThree(({ scene }) => {
210 | const grid = new THREE.GridHelper(1000, 100);
211 | scene.add(grid);
212 |
213 | return grid;
214 | });
215 |
216 | return null;
217 | };
218 | ```
219 |
220 | Notice a few things here:
221 | - Our `setup` param method signature destructures `scene` since that's all we care about
222 | - We didn't pass `destroy` param, so `useThree` will just call `scene.remove` with `grid`, since that's what we returned from `setup`.
223 | - The component renders `null`, otherwise React will throw an error.
224 | - We don't care about props changing, so we don't store the return value of `useThree` (which would give us access to `grid` object through `getEntity`).
225 |
226 | If we did care about the props changing, we could destructure `getEntity` from the return value of `useThree` and use it in another effect that triggers when props change:
227 |
228 | ```js
229 | const Grid = props => {
230 | const { color } = props
231 | const { getEntity } = useThree(…)
232 |
233 | useEffect(
234 | () => {
235 | const grid = getEntity()
236 | grid.material.color.set(color)
237 | },
238 | [color],
239 | )
240 | …
241 | }
242 | ```
243 |
244 | If we wanted to do something specific on unmount, we can pass a `destroy` function as the 2nd argument. Perhaps our component is complex and has several three.js objects and our `setup` function returned an object containing all of them:
245 | ```js
246 | const ComplexThreeComponent = () => {
247 | const getEntity = useThree(
248 | ({ scene }) => {
249 | …
250 | return {
251 | arms,
252 | body,
253 | leg,
254 | }
255 | },
256 | ({ scene }, entity) => {
257 | const { arms, body, leg } = entity
258 | scene.remove(arms)
259 | scene.remove(body)
260 | scene.remove(leg)
261 | }
262 | })
263 | …
264 | };
265 | ```
266 |
267 | If we want to setup a camera control, we can create a component that uses `camera` and `canvas` in its `setup` function:
268 |
269 | ```js
270 | const CameraControls = () => {
271 | useThree(({ camera, canvas }) => {
272 | const controls = new OrbitControls(camera, canvas)
273 | …
274 | })
275 | …
276 | }
277 | ```
278 |
279 | ## Animations / `requestAnimationFrame`
280 | `ThreeJSManager` has a component state variable called `timer` which it provides on the context. We can create effects that use this, same as what we've already done for props. Here's how it looks to rotate our simple cube:
281 |
282 | ```js
283 | const Cube = props => {
284 | const { getEntity, timer } = useThree(…)
285 |
286 | useEffect(
287 | () => {
288 | const cube = getEntity()
289 | cube.rotation.x += .01
290 | cube.rotation.z += .01
291 | },
292 | [timer],
293 | )
294 | …
295 | }
296 | ```
297 |
298 | ## Summary
299 | At a high level what we've done is created React components that don't render anything and *just* have side effects, in this case side effects all relating to calling the three.js library, but the same concept could be applied to anything.
300 |
301 | To manage the framework of side effects we created a component which provides a bunch of objects in a React `Context` that we can perform our side effects on.
302 |
303 | We used React's new experimental hooks feature to separate the concerns of the different side effects, and control when each gets run with a high level of granularity. We have could have achieved similar results with the classic lifecycle methods, but not as declaratively.
304 |
305 |
306 | ## Known Limitations
307 | - Currently there's no way to switch the `scene` outside of rendering a different `ThreeJSManager` component
308 | - Changing the props for `ThreeJSManager` doesn't have any effect since it only uses them on mount
309 | - The scene is always rerendered with `requestAnimationFrame`, it's needed for the props changing on a `useThree` component to take effect.
310 | - The function passed to `requestAnimationFrame` doesn't actually trigger the `timer` effects in our components directly, so profiling the rendering performance could be harder
311 |
312 | ## Caveat
313 |
314 | This is mostly an experiment to see what can be done with the new React hooks, but not intended for production-level use, given the "experimental" status of hooks.
315 |
316 | ## Inspiration
317 | - [How to use plain Three.js in your React apps](https://itnext.io/how-to-use-plain-three-js-in-your-react-apps-417a79d926e0)
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-three-hook",
3 | "homepage": "https://aarosil.github.io/react-three-hook",
4 | "version": "0.1.0",
5 | "private": true,
6 | "dependencies": {
7 | "gh-pages": "^2.0.1",
8 | "react": "16.7.0-alpha.2",
9 | "react-dom": "16.7.0-alpha.2",
10 | "react-router": "^4.3.1",
11 | "react-router-dom": "^4.3.1",
12 | "react-scripts": "2.1.2",
13 | "three": "^0.99.0",
14 | "three-orbit-controls": "^82.1.0",
15 | "topojson": "^3.0.2"
16 | },
17 | "scripts": {
18 | "start": "react-scripts start",
19 | "build": "react-scripts build",
20 | "test": "react-scripts test",
21 | "eject": "react-scripts eject",
22 | "predeploy": "yarn run build",
23 | "deploy": "gh-pages -d build"
24 | },
25 | "eslintConfig": {
26 | "extends": "react-app"
27 | },
28 | "browserslist": [
29 | ">0.2%",
30 | "not dead",
31 | "not ie <= 11",
32 | "not op_mini all"
33 | ]
34 | }
35 |
--------------------------------------------------------------------------------
/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |