Skip to content

Instantly share code, notes, and snippets.

@skabbes
Last active May 5, 2021 20:57
Show Gist options
  • Save skabbes/0bfa0a969aac8ec13f716dda1ad2ab43 to your computer and use it in GitHub Desktop.
Save skabbes/0bfa0a969aac8ec13f716dda1ad2ab43 to your computer and use it in GitHub Desktop.
react-navigation with react-native web webpack config

These files aren't complete, but they are plucked out from a successful (closed-source) usage of react-navigation 1.5.8 with react-native-web@latest.

While a blog post would be nice when I have a minute free, I'd like to give this to someone who wants to give it a try before I do.

import React from 'react';
import PropTypes from 'prop-types';
import { NavigationActions, addNavigationHelpers } from 'react-navigation';
const publicPath = '/m/';
// currentAction turns the URL pathname and url parameters like this
// /chat/photo?0.chatId=1&1.photoId=2
// into a react-navigation action to reset the chat parameters
// [
// { routeName: 'chat', params: { chatId: '1' } },
// { routeName: 'photo', params: { photoId: '2' } },
// ]
function currentAction(router) {
if (typeof window === 'undefined') {
return router.getActionForPathAndParams('', {});
}
let { pathname } = window.location;
const { search } = window.location;
if (pathname.indexOf(publicPath) === 0) {
pathname = pathname.substr(publicPath.length);
} else {
pathname = pathname.substr(1);
}
let routes = null;
pathname = pathname.replace(/\/+/g, '/');
if (pathname === '/') {
routes = [{ routeName: 'landing', params: {} }];
} else {
routes = pathname.split('/').map((routeName) => {
return { routeName, params: {} };
});
if (pathname.endsWith('/')) {
routes = routes.slice(0, -1);
}
}
// Parse the URL parameters into key value pairs
const params = (search || '?')
.substr(1)
.split('&')
.map((pair) => {
const eqIdx = pair.indexOf('=');
let key;
let value;
if (eqIdx >= 0) {
key = decodeURIComponent(pair.substr(0, eqIdx));
value = decodeURIComponent(pair.substr(eqIdx + 1));
} else {
key = decodeURIComponent(pair);
value = null;
}
return { key, value };
});
// Distribute the url parmeters into routes represented
// Example:
// /a/b/c?0.id=23&1.name=Steven
// becomes
// StackRouter: [
// { routeName: 'a', params: { id: '23' } },
// { routeName: 'b', params: { name: 'Steven' } },
// { routeName: 'c', params: {} },
// ]
for (const p of params) {
// If this is an indexed parameter
const pIdx = p.key.indexOf('.');
if (pIdx > -1) {
const routeIndex = parseInt(p.key.substr(0, pIdx), 10);
if (routes[routeIndex] && routes[routeIndex].params) {
const realKey = p.key.substr(pIdx + 1);
routes[routeIndex].params[realKey] = p.value;
}
}
}
// Special case for only 1 route, no nesting needed
if (routes.length === 1) {
return router.getActionForPathAndParams(routes[0].routeName, routes[0].params);
}
return NavigationActions.reset({
index: routes.length - 1,
actions: routes.map(r => NavigationActions.navigate(r)),
});
}
function makeUri(state) {
let path = state.routes.map(r => r.routeName).join('/');
const params = [];
// We only use a stack router, this is a convention for mapping url paths back into a route stack
//
// StackRouter: [
// { routeName: 'a', params: { id: '23' } },
// { routeName: 'b', params: { name: 'Steven' } },
// { routeName: 'c', params: {} },
// ]
// becomes
// /a/b/c?0.id=23&1.name=Steven
state.routes.forEach((r, i) => {
Object.keys(r.params || {}).sort().forEach((name) => {
const key = `${i}.${name}`;
const value = r.params[name];
params.push({ key, value });
});
});
let qs = params.map(p => `${encodeURIComponent(p.key)}=${encodeURIComponent(p.value)}`).join('&');
if (qs) {
qs = `?${qs}`;
}
if (path === 'landing') {
path = '';
}
return `${publicPath}${path}${qs}`;
}
export default (NavigationAwareView) => {
const initialAction = currentAction(NavigationAwareView.router);
const initialState = NavigationAwareView.router.getStateForAction(initialAction);
const initialUri = makeUri(initialState);
if (typeof window !== 'undefined' && window.location.pathname + window.location.search !== initialUri) {
window.history.replaceState({}, initialState.title, initialUri);
}
console.log({ initialAction, initialState }); // eslint-disable-line no-console
class NavigationContainer extends React.Component {
state = initialState;
_actionEventSubscribers = new Set();
componentDidMount() {
const navigation = addNavigationHelpers({
state: this.state.routes[this.state.index],
dispatch: this.dispatch,
});
document.title = NavigationAwareView.router.getScreenOptions(
navigation,
).title;
window.onpopstate = e => {
e.preventDefault();
const action = NavigationActions.back();
if (action) {
this.dispatch(action);
}
};
}
componentWillUpdate(props, state) {
const uri = makeUri(state);
if (window.location.pathname + window.location.search !== uri) {
window.history.pushState({}, state.title, uri);
}
const navigation = addNavigationHelpers({
state: state.routes[state.index],
dispatch: this.dispatch,
});
document.title = NavigationAwareView.router.getScreenOptions(navigation).title;
}
dispatch = action => {
const state = NavigationAwareView.router.getStateForAction(action, this.state);
/* eslint-disable no-console */
if (!state) {
console.log('Dispatched action did not change state: ', { action });
} else if (console.group) {
console.group('Navigation Dispatch: ');
console.log('Action: ', action);
console.log('New State: ', state);
console.log('Last State: ', this.state);
console.groupEnd();
} else {
console.log('Navigation Dispatch: ', {
action,
newState: state,
lastState: this.state,
});
}
/* eslint-enable no-console */
if (!state) {
return true;
}
if (state !== this.state) {
this.setState(state);
return true;
}
return false;
};
render() {
const navigation = addNavigationHelpers({
state: this.state,
dispatch: this.dispatch,
addListener: (eventName, handler) => {
if (eventName !== 'action') {
return { remove: () => {} };
}
this._actionEventSubscribers.add(handler);
return {
remove: () => {
this._actionEventSubscribers.delete(handler);
},
};
},
});
return (
<NavigationAwareView {...this.props} navigation={navigation} />
);
}
getURIForAction = action => {
console.warn('getURIForAction: Dont really expect this to happen'); // eslint-disable-line no-console
const state =
NavigationAwareView.router.getStateForAction(action, this.state) ||
this.state;
const { path } = NavigationAwareView.router.getPathAndParamsForState(state);
return `${publicPath}${path}`;
};
getActionForPathAndParams = (path, params) => {
console.warn('getActionForPathAndParams: Dont really expect this to happen'); // eslint-disable-line no-console
return NavigationAwareView.router.getActionForPathAndParams(path, params);
};
getChildContext() {
return {
getActionForPathAndParams: this.getActionForPathAndParams,
getURIForAction: this.getURIForAction,
dispatch: this.dispatch,
};
}
}
NavigationContainer.childContextTypes = {
getActionForPathAndParams: PropTypes.func.isRequired,
getURIForAction: PropTypes.func.isRequired,
dispatch: PropTypes.func.isRequired,
};
NavigationContainer.propTypes = {
uriPrefix: PropTypes.string,
onNavigationStateChange: PropTypes.func,
};
return NavigationContainer;
};
// I had no use for this API, but it needed to exist to prevent build failures
module.exports = {};
/* eslint global-require: 0 */
module.exports = {
// Core
get createNavigationContainer() {
return require('./node_modules/react-navigation/src/createNavigationContainer').default;
},
get StateUtils() {
return require('./node_modules/react-navigation/src/StateUtils').default;
},
get addNavigationHelpers() {
return require('./node_modules/react-navigation/src/addNavigationHelpers').default;
},
get NavigationActions() {
return require('./node_modules/react-navigation/src/NavigationActions').default;
},
// Navigators
get createNavigator() {
return require('./node_modules/react-navigation/src/navigators/createNavigator').default;
},
get StackNavigator() {
return require('./node_modules/react-navigation/src/navigators/StackNavigator').default;
},
// Routers
get StackRouter() {
return require('./node_modules/react-navigation/src/routers/StackRouter').default;
},
get TabRouter() {
return require('./node_modules/react-navigation/src/routers/TabRouter').default;
},
// HOCs
get withNavigation() {
return require('./node_modules/react-navigation/src/views/withNavigation').default;
},
};
const path = require('path');
const _ = require('lodash');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
// This is needed for webpack to compile JavaScript.
// Many OSS React Native packages are not compiled to ES5 before being
// published. If you depend on uncompiled packages they may cause webpack build
// errors. To fix this webpack can be configured to compile to the necessary
// `node_module`.
const babelLoaderConfig = {
test: /\.js$/,
// Add every directory that needs to be compiled by Babel during the build
include: [
path.resolve(__dirname, 'my_code'),
// These are the CRNA libraries that work, if you just babel them.
path.resolve(__dirname, 'node_modules/react-native-calendars'),
path.resolve(__dirname, 'node_modules/react-native-safe-area-view'),
path.resolve(__dirname, 'node_modules/react-native-tab-view'),
path.resolve(__dirname, 'node_modules/react-navigation'),
],
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true,
plugins: ['react-native-web', 'transform-flow-strip-types'],
// The 'react-native' preset is recommended (or use your own .babelrc)
presets: ['react-native'],
},
},
};
// This is needed for webpack to import static images in JavaScript files
const imgLoader = {
test: /\.(gif|jpe?g|png)$/,
use: {
loader: 'url-loader',
options: {
useRelativePath: false,
name: '[sha512:hash:base62:5].[ext]',
limit: 4096,
outputPath: 'img/',
publicPath: '/',
},
},
};
const svgLoader = {
test: /\.(svg)$/,
use: {
loader: 'file-loader',
options: {
useRelativePath: false,
name: '[sha512:hash:base62:5].[ext]',
outputPath: 'img/',
publicPath: '/',
},
},
};
const iconLoader = {
test: /\.ttf$/,
use: {
loader: 'url-loader',
options: {
useRelativePath: false,
name: '[sha512:hash:base62:5].[ext]',
limit: 4096,
outputPath: 'font/',
publicPath: '/',
},
},
include: [
path.resolve(__dirname, 'node_modules/react-native-vector-icons'),
],
};
const baseConfig = {
output: {
path: path.resolve(__dirname, 'build'),
filename: 'my_code.js',
},
module: {
rules: [
babelLoaderConfig,
iconLoader,
svgLoader,
imgLoader,
],
},
entry: ['babel-polyfill', 'my_code/main'],
plugins: [
new HtmlWebpackPlugin({
filename: 'm.html',
template: 'my_code/index.html',
inject: false,
}),
],
resolve: {
alias: {
// Even though the plugin react-native-web/babel solves this case
// It is necessary to alias so that resolves inside of react-native-vector-icons
// works too
'react-native': 'react-native-web',
'react-navigation': path.resolve(__dirname, './react-navigation.web.js'),
'react-native-web/dist/exports/DeviceInfo': path.resolve('./react-native-web-device-info.web.js'),
},
// If you're working on a multi-platform React Native app, web-specific
// module implementations should be written in files using the extension
// `.web.js`.
extensions: ['.web.js', '.js'],
},
devServer: {
historyApiFallback: true,
},
};
function buildConfig(env) {
let stage;
if (env.prod) {
stage = 'prod';
} else if (env.qa) {
stage = 'qa';
} else if (env.dev) {
stage = 'dev';
} else {
throw new Error('Unknown stage, plese specify --env.{dev|qa|prod}');
}
const isLocal = Boolean(env.dev);
const extraPlugins = [
// `process.env.NODE_ENV === 'production'` must be `true` for production
// builds to eliminate development checks and reduce build size. You may
// wish to include additional optimizations.
new webpack.DefinePlugin({
__DEV__: JSON.stringify(isLocal ? true : false),
'process.env.NODE_ENV': JSON.stringify(isLocal ? 'development' : 'production'),
}),
];
const newConfig = _.cloneDeep(baseConfig);
newConfig.plugins = [...baseConfig.plugins, ...extraPlugins];
return [newConfig];
}
module.exports = buildConfig;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment