├── .babelrc ├── .eslintrc ├── .gitignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── README.md ├── package.json ├── src ├── App.jsx ├── VisWithClass.jsx ├── VisWithHooks.jsx ├── __tests__ │ ├── App.jsx │ ├── __mocks__ │ │ └── fileMock.js │ ├── __setup__ │ │ └── enzyme.js │ └── __snapshots__ │ │ └── App.jsx.snap ├── index.css ├── index.html └── index.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": [ 4 | "@babel/plugin-proposal-object-rest-spread", 5 | "@babel/plugin-proposal-class-properties", 6 | "@babel/plugin-transform-runtime" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": ["airbnb", "plugin:prettier/recommended", "prettier/react"], 4 | "env": { 5 | "jest": true, 6 | "browser": true 7 | }, 8 | "rules": { 9 | "semi": [2, "never"], 10 | "react/jsx-filename-extension": [ 11 | "error", 12 | { "extensions": [".js", ".jsx"] } 13 | ], 14 | "react/prefer-stateless-function": 0, 15 | "no-useless-constructor": 0 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v1.2.0 2 | 3 | - Added Jest integration 4 | - Added support for `async` / `await` 5 | 6 | ## v1.1.0 7 | 8 | - Upgrade to Babel 7 (using `npx babel-upgrade --write`) 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React with three.js example 2 | 3 | See the [demo](https://willbamford.github.io/react-with-threejs-example/) 4 | 5 | Created in response to [this question](https://stackoverflow.com/questions/41248287/how-to-connect-threejs-to-react) on Stack Overflow. 6 | 7 | ## Using React Hooks 8 | 9 | Take a look at [`VisWithHooks.jsx`](src/VisWithHooks.jsx) for the same example implemented using [React Hooks](https://reactjs.org/docs/hooks-overview.html). 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-with-threejs", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "start": "webpack-dev-server --mode development --open", 7 | "dev": "webpack --mode development", 8 | "build": "webpack --mode production", 9 | "lint": "eslint ./src", 10 | "fix": "eslint ./src --fix", 11 | "test": "jest", 12 | "deploy": "gh-pages -d dist" 13 | }, 14 | "prettier": { 15 | "semi": false, 16 | "singleQuote": true, 17 | "trailingComma": "all" 18 | }, 19 | "jest": { 20 | "roots": [ 21 | "./src" 22 | ], 23 | "setupFiles": [ 24 | "./src/__tests__/__setup__/enzyme.js" 25 | ], 26 | "testPathIgnorePatterns": [ 27 | "/node_modules/", 28 | "/__mocks__/", 29 | "/__setup__/" 30 | ], 31 | "moduleNameMapper": { 32 | "^.+\\.(css|svg)$": "/src/__tests__/__mocks__/fileMock.js" 33 | } 34 | }, 35 | "author": "Will Bamford", 36 | "license": "MIT", 37 | "private": false, 38 | "devDependencies": { 39 | "@babel/core": "^7.0.0", 40 | "@babel/plugin-proposal-class-properties": "^7.1.0", 41 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0", 42 | "@babel/plugin-transform-runtime": "^7.1.0", 43 | "@babel/preset-env": "^7.0.0", 44 | "@babel/preset-react": "^7.0.0", 45 | "babel-core": "^7.0.0-bridge.0", 46 | "babel-eslint": "^8.2.3", 47 | "babel-jest": "^23.6.0", 48 | "babel-loader": "^8.0.0", 49 | "css-loader": "^0.28.11", 50 | "enzyme": "^3.7.0", 51 | "enzyme-adapter-react-16": "^1.6.0", 52 | "enzyme-to-json": "^3.3.4", 53 | "eslint": "^4.9.0", 54 | "eslint-config-airbnb": "16.1.0", 55 | "eslint-config-prettier": "^2.9.0", 56 | "eslint-plugin-import": "^2.7.0", 57 | "eslint-plugin-jsx-a11y": "^6.0.2", 58 | "eslint-plugin-prettier": "^2.6.2", 59 | "eslint-plugin-react": "^7.4.0", 60 | "file-loader": "^1.1.11", 61 | "gh-pages": "^2.0.1", 62 | "html-webpack-plugin": "^3.2.0", 63 | "jest": "^23.6.0", 64 | "mini-css-extract-plugin": "^0.4.0", 65 | "prettier": "^1.14.0", 66 | "react-test-renderer": "^16.5.2", 67 | "webpack": "^4.11.1", 68 | "webpack-cli": "^3.0.2", 69 | "webpack-dev-server": "^3.1.4" 70 | }, 71 | "dependencies": { 72 | "@babel/runtime": "^7.1.2", 73 | "react": "16.7.0-alpha.2", 74 | "react-dom": "^16.7.0-alpha.2", 75 | "three": "^0.98.0" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | // import Vis from './VisWithClass' 4 | import Vis from './VisWithHooks' 5 | 6 | const App = () => ( 7 |
8 | 9 |
10 | ) 11 | 12 | export default App 13 | -------------------------------------------------------------------------------- /src/VisWithClass.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import * as THREE from 'three' 3 | 4 | class VisWithClass extends Component { 5 | componentDidMount() { 6 | const width = this.mount.clientWidth 7 | const height = this.mount.clientHeight 8 | 9 | const scene = new THREE.Scene() 10 | const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000) 11 | const renderer = new THREE.WebGLRenderer({ antialias: true }) 12 | const geometry = new THREE.BoxGeometry(1, 1, 1) 13 | const material = new THREE.MeshBasicMaterial({ color: 0xff00ff }) 14 | const cube = new THREE.Mesh(geometry, material) 15 | 16 | camera.position.z = 4 17 | scene.add(cube) 18 | renderer.setClearColor('#000000') 19 | renderer.setSize(width, height) 20 | 21 | this.scene = scene 22 | this.camera = camera 23 | this.renderer = renderer 24 | this.material = material 25 | this.cube = cube 26 | 27 | window.addEventListener('resize', this.handleResize) 28 | 29 | this.mount.appendChild(this.renderer.domElement) 30 | this.start() 31 | } 32 | 33 | componentWillUnmount() { 34 | window.removeEventListener('resize') 35 | this.stop() 36 | this.mount.removeChild(this.renderer.domElement) 37 | } 38 | 39 | handleResize = () => { 40 | const width = this.mount.clientWidth 41 | const height = this.mount.clientHeight 42 | this.renderer.setSize(width, height) 43 | this.camera.aspect = width / height 44 | this.camera.updateProjectionMatrix() 45 | } 46 | 47 | start = () => { 48 | if (!this.frameId) { 49 | this.frameId = requestAnimationFrame(this.animate) 50 | } 51 | } 52 | 53 | stop = () => { 54 | cancelAnimationFrame(this.frameId) 55 | } 56 | 57 | animate = () => { 58 | this.cube.rotation.x += 0.01 59 | this.cube.rotation.y += 0.01 60 | 61 | this.renderScene() 62 | this.frameId = window.requestAnimationFrame(this.animate) 63 | } 64 | 65 | renderScene = () => { 66 | this.renderer.render(this.scene, this.camera) 67 | } 68 | 69 | render() { 70 | return ( 71 |
{ 74 | this.mount = mount 75 | }} 76 | /> 77 | ) 78 | } 79 | } 80 | 81 | export default VisWithClass 82 | -------------------------------------------------------------------------------- /src/VisWithHooks.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react' 2 | import * as THREE from 'three' 3 | 4 | const VisWithHooks = () => { 5 | const mount = useRef(null) 6 | const [isAnimating, setAnimating] = useState(true) 7 | const controls = useRef(null) 8 | 9 | useEffect(() => { 10 | let width = mount.current.clientWidth 11 | let height = mount.current.clientHeight 12 | let frameId 13 | 14 | const scene = new THREE.Scene() 15 | const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000) 16 | const renderer = new THREE.WebGLRenderer({ antialias: true }) 17 | const geometry = new THREE.BoxGeometry(1, 1, 1) 18 | const material = new THREE.MeshBasicMaterial({ color: 0xff00ff }) 19 | const cube = new THREE.Mesh(geometry, material) 20 | 21 | camera.position.z = 4 22 | scene.add(cube) 23 | renderer.setClearColor('#000000') 24 | renderer.setSize(width, height) 25 | 26 | const renderScene = () => { 27 | renderer.render(scene, camera) 28 | } 29 | 30 | const handleResize = () => { 31 | width = mount.current.clientWidth 32 | height = mount.current.clientHeight 33 | renderer.setSize(width, height) 34 | camera.aspect = width / height 35 | camera.updateProjectionMatrix() 36 | renderScene() 37 | } 38 | 39 | const animate = () => { 40 | cube.rotation.x += 0.01 41 | cube.rotation.y += 0.01 42 | 43 | renderScene() 44 | frameId = window.requestAnimationFrame(animate) 45 | } 46 | 47 | const start = () => { 48 | if (!frameId) { 49 | frameId = requestAnimationFrame(animate) 50 | } 51 | } 52 | 53 | const stop = () => { 54 | cancelAnimationFrame(frameId) 55 | frameId = null 56 | } 57 | 58 | mount.current.appendChild(renderer.domElement) 59 | window.addEventListener('resize', handleResize) 60 | start() 61 | 62 | controls.current = { start, stop } 63 | 64 | return () => { 65 | stop() 66 | window.removeEventListener('resize', handleResize) 67 | mount.current.removeChild(renderer.domElement) 68 | 69 | scene.remove(cube) 70 | geometry.dispose() 71 | material.dispose() 72 | } 73 | }, []) 74 | 75 | useEffect( 76 | () => { 77 | if (isAnimating) { 78 | controls.current.start() 79 | } else { 80 | controls.current.stop() 81 | } 82 | }, 83 | [isAnimating], 84 | ) 85 | 86 | /* eslint-disable 87 | jsx-a11y/click-events-have-key-events, 88 | jsx-a11y/no-static-element-interactions 89 | */ 90 | return ( 91 |
setAnimating(!isAnimating)} 95 | /> 96 | ) 97 | } 98 | 99 | export default VisWithHooks 100 | -------------------------------------------------------------------------------- /src/__tests__/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | import toJson from 'enzyme-to-json' 4 | 5 | import App from '../App' 6 | 7 | const setup = (testProps = {}) => { 8 | const props = { 9 | ...testProps, 10 | } 11 | 12 | const wrapper = shallow() 13 | 14 | return { 15 | props, 16 | wrapper, 17 | } 18 | } 19 | 20 | describe('App', () => { 21 | it('renders matching snapshot', () => { 22 | const { wrapper } = setup() 23 | 24 | expect(toJson(wrapper)).toMatchSnapshot() 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /src/__tests__/__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | export default 'file-mock' 2 | -------------------------------------------------------------------------------- /src/__tests__/__setup__/enzyme.js: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme' 2 | import Adapter from 'enzyme-adapter-react-16' 3 | 4 | configure({ adapter: new Adapter() }) 5 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/App.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`App renders matching snapshot 1`] = ` 4 |
7 |

8 | It is working! 9 |

10 |
13 | LBA 22 |
23 |
24 | `; 25 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | } 4 | 5 | *, 6 | *:before, 7 | *:after { 8 | box-sizing: inherit; 9 | } 10 | 11 | body { 12 | font-family: sans-serif; 13 | padding: 0; 14 | margin: 0; 15 | } 16 | 17 | .vis { 18 | position: fixed; 19 | top: 0; 20 | right: 0; 21 | bottom: 0; 22 | left: 0; 23 | } 24 | 25 | .visually-hidden { 26 | position: absolute; 27 | overflow: hidden; 28 | clip: rect(0 0 0 0); 29 | height: 1px; 30 | width: 1px; 31 | margin: -1px; 32 | padding: 0; 33 | border: 0; 34 | } 35 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React with three.js Example 7 | 8 | 9 |

10 | An example demonstrating three.js integration with React 11 |

12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | 4 | import App from './App' 5 | 6 | import './index.css' 7 | 8 | ReactDOM.render(, document.getElementById('app')) 9 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebPackPlugin = require('html-webpack-plugin') 2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 3 | 4 | module.exports = { 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.(js|jsx)$/, 9 | exclude: /node_modules/, 10 | use: 'babel-loader', 11 | }, 12 | { 13 | test: /\.css$/, 14 | use: [MiniCssExtractPlugin.loader, 'css-loader'], 15 | }, 16 | { 17 | test: /\.svg$/, 18 | use: 'file-loader', 19 | }, 20 | ], 21 | }, 22 | plugins: [ 23 | new HtmlWebPackPlugin({ 24 | template: './src/index.html', 25 | filename: './index.html', 26 | }), 27 | new MiniCssExtractPlugin({ 28 | filename: '[name].css', 29 | chunkFilename: '[id].css', 30 | }), 31 | ], 32 | resolve: { 33 | extensions: ['.js', '.jsx'], 34 | }, 35 | } 36 | --------------------------------------------------------------------------------