├── .babelrc ├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── Cargo.toml ├── README.md ├── dist ├── index.html ├── react-static-routes.js └── robots.txt ├── install.sh ├── package.json ├── public └── robots.txt ├── src ├── components │ └── App.js ├── ducks │ ├── app.js │ ├── helper.js │ ├── index.js │ └── root.js ├── index.js ├── lib.rs ├── main.rs └── routes │ ├── 404.js │ └── index.js ├── static.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-static/.babelrc", 3 | "env": { 4 | "production": { 5 | "plugins": [ 6 | ["emotion", {"sourceMap": false, "hoist": true, "autoLabel": true}] 7 | ] 8 | }, 9 | "development": { 10 | "plugins": [ 11 | ["emotion", {"sourceMap": true, "hoist": true, "autoLabel": true}] 12 | ] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "standard", 5 | "plugin:react/recommended", 6 | "prettier", 7 | "prettier/react", 8 | "prettier/standard" 9 | ], 10 | "plugins": ["react", "prettier", "standard"], 11 | "parserOptions": { 12 | "sourceType": "module", 13 | "ecmaFeatures": { 14 | "jsx": true 15 | } 16 | }, 17 | "env": { 18 | "es6": true, 19 | "node": true, 20 | "browser": true 21 | }, 22 | "rules": { 23 | "prettier/prettier": [ 24 | "error", 25 | { 26 | "printWidth": 80, 27 | "tabWidth": 2, 28 | "singleQuote": true, 29 | "trailingComma": "all", 30 | "bracketSpacing": false, 31 | "semi": false, 32 | "useTabs": false, 33 | "parser": "babylon", 34 | "jsxBracketSameLine": true 35 | } 36 | ], 37 | "eqeqeq": ["error", "always"], 38 | "space-before-function-paren": 0, 39 | "generator-star-spacing": 0, 40 | "react/prop-types": 0, 41 | "react/display-name": 0, 42 | "import/no-unresolved": [1, {"commonjs": true, "amd": true}], 43 | "import/named": 2, 44 | "import/namespace": 2, 45 | "import/default": 2, 46 | "import/export": 2 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node,rust,macos 3 | 4 | ### macOS ### 5 | *.DS_Store 6 | .AppleDouble 7 | .LSOverride 8 | 9 | # Icon must end with two \r 10 | Icon 11 | 12 | # Thumbnails 13 | ._* 14 | 15 | # Files that might appear in the root of a volume 16 | .DocumentRevisions-V100 17 | .fseventsd 18 | .Spotlight-V100 19 | .TemporaryItems 20 | .Trashes 21 | .VolumeIcon.icns 22 | .com.apple.timemachine.donotpresent 23 | 24 | # Directories potentially created on remote AFP share 25 | .AppleDB 26 | .AppleDesktop 27 | Network Trash Folder 28 | Temporary Items 29 | .apdisk 30 | 31 | ### Node ### 32 | # Logs 33 | logs 34 | *.log 35 | npm-debug.log* 36 | yarn-debug.log* 37 | yarn-error.log* 38 | 39 | # Runtime data 40 | pids 41 | *.pid 42 | *.seed 43 | *.pid.lock 44 | 45 | # Directory for instrumented libs generated by jscoverage/JSCover 46 | lib-cov 47 | 48 | # Coverage directory used by tools like istanbul 49 | coverage 50 | 51 | # nyc test coverage 52 | .nyc_output 53 | 54 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 55 | .grunt 56 | 57 | # Bower dependency directory (https://bower.io/) 58 | bower_components 59 | 60 | # node-waf configuration 61 | .lock-wscript 62 | 63 | # Compiled binary addons (http://nodejs.org/api/addons.html) 64 | build/Release 65 | 66 | # Dependency directories 67 | node_modules/ 68 | jspm_packages/ 69 | 70 | # Typescript v1 declaration files 71 | typings/ 72 | 73 | # Optional npm cache directory 74 | .npm 75 | 76 | # Optional eslint cache 77 | .eslintcache 78 | 79 | # Optional REPL history 80 | .node_repl_history 81 | 82 | # Output of 'npm pack' 83 | *.tgz 84 | 85 | # Yarn Integrity file 86 | .yarn-integrity 87 | 88 | # dotenv environment variables file 89 | .env 90 | 91 | 92 | ### Rust ### 93 | # Generated by Cargo 94 | # will have compiled files and executables 95 | /target/ 96 | 97 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 98 | # More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock 99 | Cargo.lock 100 | 101 | # These are backup files generated by rustfmt 102 | **/*.rs.bk 103 | 104 | 105 | # End of https://www.gitignore.io/api/node,rust,macos -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Phoomparin Mano"] 3 | name = "react-rust" 4 | version = "0.1.0" 5 | 6 | [dependencies] 7 | stdweb = "0.3.0" 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebAssembly Modules in React Apps via Rust 2 | 3 | We're going to use Rust, Cargo Web and Stdweb to provide WebAssembly Modules, 4 | then use them in react-static. 5 | 6 | ### Usage 7 | 8 | ```sh 9 | # Install Rust and Cargo Web 10 | ./install.sh 11 | 12 | # Install Node Modules 13 | yarn --dev 14 | 15 | # Start it up! 16 | yarn start 17 | ``` 18 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | React 💕 Rust
-------------------------------------------------------------------------------- /dist/react-static-routes.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react' 3 | import { Route } from 'react-router-dom' 4 | import universal, { setHasBabelPlugin } from 'react-universal-component' 5 | 6 | setHasBabelPlugin() 7 | 8 | const universalOptions = { 9 | loading: () => null, 10 | error: () => null, 11 | } 12 | 13 | const t_0 = universal(import('../src/routes/index'), universalOptions) 14 | const t_1 = universal(import('../src/routes/404'), universalOptions) 15 | 16 | // Template Map 17 | const templateMap = { 18 | t_0, 19 | t_1 20 | } 21 | 22 | // Template Tree 23 | const templateTree = {c:{"404":{t:"t_1"},"/":{t:"t_0"}}} 24 | 25 | // Get template for given path 26 | const getComponentForPath = path => { 27 | const parts = path === '/' ? ['/'] : path.split('/').filter(d => d) 28 | let cursor = templateTree 29 | try { 30 | parts.forEach(part => { 31 | cursor = cursor.c[part] 32 | }) 33 | return templateMap[cursor.t] 34 | } catch (e) { 35 | return false 36 | } 37 | } 38 | 39 | if (typeof document !== 'undefined') { 40 | window.reactStaticGetComponentForPath = getComponentForPath 41 | } 42 | 43 | export default class Routes extends Component { 44 | render () { 45 | const { component: Comp, render, children } = this.props 46 | const renderProps = { 47 | templateMap, 48 | templateTree, 49 | getComponentForPath 50 | } 51 | if (Comp) { 52 | return ( 53 | 56 | ) 57 | } 58 | if (render || children) { 59 | return (render || children)(renderProps) 60 | } 61 | 62 | // This is the default auto-routing renderer 63 | return ( 64 | { 65 | let Comp = getComponentForPath(props.location.pathname) 66 | if (!Comp) { 67 | Comp = getComponentForPath('404') 68 | } 69 | return Comp && 70 | }} /> 71 | ) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /dist/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | # Install Rust 2 | curl https://sh.rustup.rs -sSf | sh 3 | rustup update 4 | 5 | # Install Nightly Rust 6 | rustup default nightly 7 | rustup update nightly 8 | 9 | # Adds the WebAssembly Target 10 | rustup target add wasm32-unknown-unknown --toolchain nightly 11 | 12 | # Install wasm-gc 13 | cargo install --force --git https://github.com/alexcrichton/wasm-gc 14 | 15 | # Install cargo-web via stable Rust 16 | # NOTE: You need stable Rust to install cargo-web on macOS, for now. 17 | cargo +stable install cargo-web 18 | 19 | # Test to see if it actually works 20 | cargo web build --message-format=json --target wasm32-unknown-unknown --release 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-static-example-basic", 3 | "version": "1.0.1", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "react-static start", 8 | "build": "react-static build", 9 | "serve": "serve dist -p 3000" 10 | }, 11 | "dependencies": { 12 | "axios": "^0.17.1", 13 | "emotion": "^8.0.12", 14 | "polished": "^1.9.0", 15 | "react": "^16.2.0", 16 | "react-dom": "^16.0.0", 17 | "react-emotion": "^8.0.12", 18 | "react-redux": "^5.0.6", 19 | "react-router": "^4.2.0", 20 | "react-static": "^4.9.0-beta.5", 21 | "recompose": "^0.26.0", 22 | "redux": "^3.7.2", 23 | "redux-persist": "^5.5.0", 24 | "redux-saga": "^0.16.0", 25 | "reselect": "^3.0.1" 26 | }, 27 | "devDependencies": { 28 | "babel-eslint": "^8.2.1", 29 | "emotion-server": "^8.0.12", 30 | "eslint": "^4.16.0", 31 | "eslint-config-prettier": "^2.9.0", 32 | "eslint-config-react-tools": "1.x.x", 33 | "eslint-config-standard": "^11.0.0-beta.0", 34 | "eslint-config-standard-react": "^5.0.0", 35 | "eslint-plugin-import": "^2.8.0", 36 | "eslint-plugin-node": "^5.2.1", 37 | "eslint-plugin-prettier": "^2.5.0", 38 | "eslint-plugin-promise": "^3.6.0", 39 | "eslint-plugin-react": "^7.5.1", 40 | "eslint-plugin-standard": "^3.0.1", 41 | "prettier": "^1.10.2", 42 | "rust-native-wasm-loader": "^0.2.7", 43 | "serve": "^6.1.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Provider} from 'react-redux' 3 | import {Router} from 'react-static' 4 | import {lifecycle} from 'recompose' 5 | import {injectGlobal} from 'react-emotion' 6 | 7 | import Routes from 'react-static-routes' 8 | 9 | import createStore from '../ducks' 10 | 11 | const store = createStore() 12 | 13 | const App = () => ( 14 | 15 | 16 | 17 | 18 | 19 | ) 20 | 21 | const enhance = lifecycle({ 22 | componentWillMount() { 23 | injectGlobal` 24 | body { 25 | margin: 0; 26 | font-family: -apple-system, BlinkMacSystemFont, sans-serif; 27 | color: #555; 28 | background: rgb(251, 252, 255); 29 | } 30 | 31 | h1, h2 { 32 | font-weight: 300; 33 | } 34 | ` 35 | }, 36 | }) 37 | 38 | export default enhance(App) 39 | -------------------------------------------------------------------------------- /src/ducks/app.js: -------------------------------------------------------------------------------- 1 | import {takeEvery, call, put} from 'redux-saga/effects' 2 | 3 | import {createReducer, Creator} from './helper' 4 | import wasm from '../main.rs' 5 | 6 | export const GET_SECRET = 'GET_SECRET' 7 | export const STORE_SECRET = 'STORE_SECRET' 8 | 9 | export const getSecret = Creator(GET_SECRET) 10 | export const storeSecret = Creator(STORE_SECRET) 11 | 12 | export function* getSecretSaga() { 13 | const lib = yield wasm 14 | 15 | const name = lib.secret() 16 | const phrase = localStorage.getItem(lib.SECRET_KEY) 17 | 18 | yield put(storeSecret({name, phrase})) 19 | } 20 | 21 | export function* appWatcherSaga() { 22 | yield takeEvery(GET_SECRET, getSecretSaga) 23 | } 24 | 25 | const initial = { 26 | secret: {}, 27 | } 28 | 29 | export default createReducer(initial, state => ({ 30 | [STORE_SECRET]: secret => ({ 31 | secret, 32 | }), 33 | })) 34 | -------------------------------------------------------------------------------- /src/ducks/helper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a reducer from an initial state and a handler function. 3 | * @param {object} initialState 4 | * @param {object} handlers - handler function which returns an object 5 | * @example state => ({ SET_NAME: name => ({...state, name}) }) 6 | */ 7 | export function createReducer(initialState, handlers) { 8 | return (state = initialState, action) => 9 | handlers(state)[action.type] 10 | ? handlers(state)[action.type](action.payload) 11 | : state 12 | } 13 | 14 | /** 15 | * Creates an action creator. 16 | * Will also put each arguments into the payload, if any. 17 | * @param {string} type - action type 18 | * @param {...string} [argNames] - action argument names 19 | * @return {function} Returns the Action Creator Function 20 | */ 21 | export function Creator(type, ...argNames) { 22 | if (argNames.length > 0) { 23 | return (...args) => { 24 | const payload = {} 25 | argNames.forEach((arg, index) => { 26 | payload[argNames[index]] = args[index] 27 | }) 28 | return {type, payload} 29 | } 30 | } 31 | return payload => (payload ? {type, payload} : {type}) 32 | } 33 | -------------------------------------------------------------------------------- /src/ducks/index.js: -------------------------------------------------------------------------------- 1 | import {compose, createStore, applyMiddleware} from 'redux' 2 | import createSagaMiddleware from 'redux-saga' 3 | import {persistStore} from 'redux-persist' 4 | 5 | import {reducers, rootSaga} from './root' 6 | 7 | /* eslint no-undef: 0 */ 8 | 9 | export default () => { 10 | const saga = createSagaMiddleware() 11 | const middleware = [saga] 12 | let composeEnhancers = compose 13 | 14 | if (typeof window !== 'undefined') { 15 | composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose 16 | } 17 | 18 | const store = createStore( 19 | reducers, 20 | composeEnhancers(applyMiddleware(...middleware)), 21 | ) 22 | 23 | // NOTE: Uncomment this to persist to Redux Store 24 | // persistStore(store) 25 | 26 | if (module.hot) { 27 | module.hot.accept(() => { 28 | const nextReducers = require('./root').reducers 29 | store.replaceReducer(nextReducers) 30 | }) 31 | } 32 | 33 | saga.run(rootSaga) 34 | 35 | return store 36 | } 37 | -------------------------------------------------------------------------------- /src/ducks/root.js: -------------------------------------------------------------------------------- 1 | import {all} from 'redux-saga/effects' 2 | import storage from 'redux-persist/lib/storage' 3 | import {persistCombineReducers} from 'redux-persist' 4 | 5 | import app, {appWatcherSaga} from './app' 6 | 7 | const config = {key: 'root', storage} 8 | 9 | export const reducers = persistCombineReducers(config, {app}) 10 | 11 | export function* rootSaga() { 12 | yield all([appWatcherSaga()]) 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import {AppContainer} from 'react-hot-loader' 4 | 5 | import App from './components/App' 6 | 7 | if (typeof document !== 'undefined') { 8 | const renderMethod = module.hot ? ReactDOM.render : ReactDOM.hydrate 9 | 10 | const render = Comp => { 11 | renderMethod( 12 | 13 | 14 | , 15 | document.getElementById('root') 16 | ) 17 | } 18 | 19 | render(App) 20 | 21 | if (module.hot) { 22 | module.hot.accept('./components/App', () => { 23 | render(require('./components/App').default) 24 | }) 25 | } 26 | } 27 | 28 | export default App 29 | 30 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heypoom/react-rust/d53f13de1d49af6d508ca82f71131b912906b948/src/lib.rs -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate stdweb; 3 | 4 | use stdweb::web::*; 5 | 6 | pub static SECRET_KEY: &str = "SUPER_DUPER_SECRET_KEY"; 7 | 8 | fn secret() -> String { 9 | let window = window(); 10 | let storage = window.local_storage(); 11 | 12 | storage.insert(SECRET_KEY, "I'm a Hog."); 13 | 14 | String::from("Hot Bog!") 15 | } 16 | 17 | fn main() { 18 | stdweb::initialize(); 19 | 20 | js! { 21 | console.log("Hello from Rust!"); 22 | 23 | fetch("https://jsonplaceholder.typicode.com").then(console.log); 24 | 25 | Module.exports.secret = @{secret}; 26 | Module.exports.SECRET_KEY = @{SECRET_KEY}; 27 | } 28 | 29 | stdweb::event_loop() 30 | } 31 | -------------------------------------------------------------------------------- /src/routes/404.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const NotFound = () =>
404 - Not Found
4 | 5 | export default NotFound 6 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled, {css} from 'react-emotion' 3 | import {connect} from 'react-redux' 4 | 5 | import {getSecret} from '../ducks/app' 6 | 7 | const Container = styled.div` 8 | padding: 2em; 9 | margin: 2em; 10 | 11 | text-align: center; 12 | box-shadow: rgba(0, 0, 0, 0.05) 0px 0px 25px; 13 | background: white; 14 | ` 15 | 16 | // prettier-ignore 17 | const Header = styled.h1` 18 | margin: 0; 19 | margin-bottom: 0.5em; 20 | 21 | ${props => props.big && css` 22 | font-size: 2.2em; 23 | font-weight: 400; 24 | `}; 25 | ` 26 | 27 | const Button = styled.button` 28 | background: #2d2d30; 29 | border-radius: 4px; 30 | border: none; 31 | box-shadow: rgba(51, 51, 51, 0.6) 0px 4px 20px -2px; 32 | color: white; 33 | cursor: pointer; 34 | font-size: 1.08em; 35 | outline: none; 36 | padding: 0.4em 0.8em; 37 | transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); 38 | 39 | &:hover { 40 | color: #555; 41 | background: white; 42 | } 43 | ` 44 | 45 | const Landing = ({secret, getSecret}) => ( 46 | 47 |
Knock, Knock. {secret.phrase}
48 |
{secret.name}
49 | 50 |
51 | ) 52 | 53 | const mapStateToProps = state => ({ 54 | secret: state.app.secret, 55 | }) 56 | 57 | const enhance = connect(mapStateToProps, {getSecret}) 58 | 59 | export default enhance(Landing) 60 | -------------------------------------------------------------------------------- /static.config.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {extractCritical} from 'emotion-server' 3 | 4 | const Document = ({Html, Head, Body, children, renderMeta}) => ( 5 | 6 | 7 | 8 | 9 |