Sharing of React code across Web, iOS, Android, Electron as Hello World example. Before you read on, just a quick shout out that there are a lot of web resources I used to build this out; I'm not a savant. I'll go back through my browser history at some point, its a scary place to be, and add shout outs.
Repo was created using react-native init MultiPlatformReact
which initially
created a folder named the same in the root project. In addition, this uses
eslint so there was an addition npm install --save-dev eslint-plugin-react
command run. The initial structure looked like:
To make the project a little easier to navigate (I hope). Ideally, the specific index files would be in their respective folders but there are issues where parts of the project expect them in the root, so they're all there for consistency. The files now reside at the following:
The goal of these components are that, to extent you can, keep react-native
in the native files and react in the plain .js
files. There's only a root
component that imports a root-render
function. The web / electron files
pickup the standard root-render.js
file. So each component built is going to
have 5 files. The root component has:
- root-render.android.js
- root-render.ios.js
- root-render.js
- root-render.native.js
- root.js
The base root file will pull in root-render.js
and looks like:
'use strict';
import Render from './root-render';
import { Component } from 'react';
export default class Root extends Component {
render () {
return Render.call(this, this.props, this.state);
}
}
but the iOS and Android platforms will look for the specific root-render.ios.js
and root-render.android.js
files, based on the platform, and both of those look like:
'use strict';
import Render from './root-render.native';
export default function () {
return Render.call(this, this.props, this.state);
}
and redirect to the root-render.native.js
file and look like:
'use strict';
import React, {
StyleSheet,
View,
Text
} from 'react-native';
export default function () {
return (
<View style={styles.container}>
<Text>Hello World</Text>
</View>
);
}
var styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: "column",
justifyContent: "center",
backgroundColor: "#303030"
}
});
Web and ELECTRON
Each of the web and electron, which uses the web code, have their own file for
the index.html
and index.electron.js
files. Webpack is the build system for
web code and will output its bundle files to the web folder. In order to not
cause issues with react native, we need to add an rn-cli.config.js
file to
blacklist the web folder for weird duplicate entry / dependency errors. Pretty
simple:
var blacklist = require('react-native/packager/blacklist');
var config = {
getBlacklistRE(platform) {
return blacklist(platform, [
/web/
]);
}
};
module.exports = config;
When trying to run the electron version, make sure to run the webpack build to write the bundle.js file to the folder for electron to pickup. The webpack dev server doesn't currently do that with this configuration.
The project uses webpack for bundling the web code and webpack-dev-server for basic hosting of the app locally. This is because the project I started this for is a static hosted app on Amazon S3 so I don't need a server in production. The config will be found by webpack automatically based on its name and looks like:
var path = require('path');
var webpack = require('webpack');
module.exports = {
entry: './index.js',
output: {
path: './web',
filename: 'bundle.js'
},
module: {
loaders: [
{
test: /.js?$/,
loader: 'babel',
exclude: /node_modules/,
query:
{
presets:['es2015','react']
}
}
]
},
debug: true,
devtool: "source-map",
plugins: [
new webpack.DefinePlugin({
__DEV__: process.env.NODE_ENV === "development"
})
]
};
The webpack config is pretty basic but does inject a nice __DEV__
variable
that you can use to hide things in development you don't quite want in prod such
as, say in a component's render function you wanted to hide a tag:
export default function () {
let forgotPassword;
// this code will show up for you locally, but not when the webpack
// production build kicks off
if (__DEV__) {
forgotPassword =
<button
style={styles.forgot}
>Forgot Password?</button>;
}
return (
<div>
<h2>Hello World</h2>
{forgotPassword}
</div>
);
}
And, the commands for all of this to work are in the package.json
file and
look like this:
"scripts": {
"start:electron": "node_modules/.bin/electron electron/index.electron.js",
"start:ios": "node_modules/react-native/packager/packager.sh",
"start:web": "NODE_ENV=development node_modules/.bin/webpack-dev-server --progress --colors --content-base web/",
"start:android": "node_modules/react-native run-android",
"build:web": "NODE_ENV=production node_modules/.bin/webpack -p",
"build:ios": "react-native bundle --entry-file index.ios.js --bundle-output ios/main.jsbundle --platform ios --dev false",
"build:android": "node_modules/react-native bundle --entry-file index.android.js --bundle-output android/app/src/main/assets/index.android.bundle --platform android --dev false"
}
At this point, the project should be working assuming you followed the Getting Started with React Native
At some point you'll probably want to add Redux because, well, the internet told
you too (just kidding it's dope!) and there's a little gotcha to remember: as of
writing this repo you have to use a specific version of the react-redux
package.
So, make sure you have "react-redux": "^3.1.2",
in your package.json
file and
then also install these:
- redux
- redux-thunk
Then, to keep things simple I'd start with a folder called redux
and have
the following files in it:
- actions.js
- reducers.js
- constants.js
- store.js
The root component would then get updated to look something like:
import Render from './root-render';
import { Component } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as actions from "../../redux/actions";
class Root extends Component {
constructor(props) {
super(props);
}
render () {
return Render.call(this, this.props, this.state);
}
}
function mapStateToProps(state) {
// add as many properties as you want mapped to props of root
return {
example: state.default.example,
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(actions, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(Root);
I'll leave the constants, actions, and reducers to you but these store file would look something like:
'use strict';
import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunk from 'redux-thunk';
import * as reducers from './reducers';
const createStoreWithMiddleware = applyMiddleware(thunk)(createStore);
const reducer = combineReducers(reducers);
const store = createStoreWithMiddleware(reducer);
export default store;
And finally your index.js
file would then wrap your root component with the
react redux magic and look like this:
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './src/redux/store';
import Root from './src/components/root/root';
ReactDOM.render(
<Provider store={store}>
<Root />
</Provider>,
document.getElementById('root')
);