I've been working on a React Native (CLI) project for a few months, and it's almost ready for release on mobile. Now, I need to make the project run on browser as well, for that I'm trying to use "react-native-web". I've encountered difficulties getting "styled-components/native", "date-fns", and other packages to work with "react-native-web".
Following the steps outlined in this guide, I successfully configured webpack for a basic App.tsx
with a Hello World message. Yet, challenges persist when incorporating trying to use styled components, even when I use "styled-components" import (without the native) by setting things like styled(View)
instead of styled.View
. It appears that webpack isn't cooperating as expected with the "styled-components/native" package.
Below is the code from my web/webpack.config.js
:
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const appDirectory = path.resolve(__dirname, '../');
const babelConfig = require('../babel.config');
const libAliases = {
'styled-components/native': require.resolve('styled-components'),
'react-native-text-input-mask': require.resolve('inputmask'),
'lottie-react-native': require.resolve('lottie-web'),
};
babelConfig.plugins[0][1].alias = {
'react-native$': require.resolve('react-native-web'),
...babelConfig.plugins[0][1].alias,
...libAliases,
};
// Babel loader configuration
const babelLoaderConfiguration = {
test: /\.(tsx|jsx|ts|js)?$/,
exclude: [
{
and: [
// babel will exclude these from transpling
path.resolve(appDirectory, 'node_modules'),
path.resolve(appDirectory, 'ios'),
path.resolve(appDirectory, 'android'),
],
// whitelisted modules to be transpiled by babel
not: [],
},
],
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true,
// Presets and plugins imported from main babel.config.js in root dir
presets: babelConfig.presets,
plugins: ['react-native-web', ...(babelConfig.plugins || [])],
},
},
};
// Image loader configuration
const imageLoaderConfiguration = {
test: /\.(gif|jpe?g|png|svg)$/,
use: {
loader: 'url-loader',
options: {
name: '[name].[ext]',
esModule: false,
},
},
};
// File loader configuration
const fileLoaderConfiguration = {
test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'fonts/',
},
},
],
};
module.exports = (argv) => {
return {
entry: path.resolve(appDirectory, 'index'),
output: {
clean: true,
path: path.resolve(appDirectory, 'web/dist'),
filename: '[name].[chunkhash].js',
sourceMapFilename: '[name].[chunkhash].map',
chunkFilename: '[id].[chunkhash].js',
},
resolve: {
extensions: [
'.web.js',
'.js',
'.web.ts',
'.ts',
'.web.jsx',
'.jsx',
'.web.tsx',
'.tsx',
],
},
module: {
rules: [
babelLoaderConfiguration,
imageLoaderConfiguration,
fileLoaderConfiguration,
{
test: /\.js$/,
include: [
path.resolve(appDirectory, 'src'), // your source folder
path.resolve(appDirectory, 'node_modules/date-fns'), // date-fns
path.resolve(appDirectory, 'node_modules/@babel/runtime'), // @babel/runtime
],
use: 'babel-loader',
},
],
},
plugins: [
// Fast refresh plugin
new ReactRefreshWebpackPlugin(),
// Plugin that takes public/index.html and injects script tags with the built bundles
new HtmlWebpackPlugin({
template: path.resolve(appDirectory, 'web/public/index.html'),
}),
// Defines __DEV__ and process.env as not being null
new webpack.DefinePlugin({
__DEV__: argv.mode !== 'production' || true,
process: { env: {} },
}),
],
optimization: {
// Split into vendor and main js files
splitChunks: {
cacheGroups: {
commons: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'initial',
},
},
},
},
};
};
And here's my babel.config.json
(that remains intact since before react-native-web):
{
"plugins": [
[
"module-resolver",
{
"root": ["./"],
"alias": {
"@assets": "./assets",
"@components": "./src/components",
"@context": "./src/context",
"@lib": "./src/lib",
"@utils": "./src/utils",
"@src": "./src"
}
}
],
"babel-plugin-styled-components",
"module:react-native-dotenv"
],
"presets": ["module:metro-react-native-babel-preset"],
"env": {
"production": {
"plugins": ["react-native-paper/babel"]
}
}
}
And here's the current stack (for making the reading easier, I'm just listing the most relevant packages):
"react": "18.2.0",
"react-native": "0.72.6",
"@react-navigation/native": "^6.1.9",
"@react-navigation/bottom-tabs": "^6.5.11",
"@react-navigation/native-stack": "^6.9.16",
"axios": "^1.6.2",
"react-hook-form": "^7.48.2",
"@hookform/resolvers": "^3.3.2",
"yup": "^1.3.2",
"zustand": "^4.4.4",
"styled-components": "^6.1.0",
"@react-native-async-storage/async-storage": "^1.19.3",
"date-fns": "^2.30.0",
"react-native-vector-icons": "^10.0.1",
"react-native-paper": "^5.11.1"
I've attempted aliasing and configuring babel, but I'm still facing challenges. Can someone help me troubleshoot this issue? Additionally, I'm considering whether Next.js might offer a solution by transpiling these packages. However, I lack clarity on the implementation process or if achieving the same outcome without Next.js is feasible.
I would greatly appreciate any guidance on effectively configuring my project to target both mobile and web platforms.
Thank you in advance for your assistance!
Edit 1
I forgot to add my output, but the thing is that I'm getting NO error whatsoever. It just refuses to render, without any verbosity.
App.tsx
:
import React from 'react';
import { Dimensions, Text, View } from 'react-native';
import styled from 'styled-components';
const { height, width } = Dimensions.get('window');
const HelloWorldContainer = styled(View)`
flex: 1;
height: ${height}px;
width: ${width}px;
justify-content: center;
align-items: center;
`;
const HelloWorldText = styled(Text)`
font-size: 58px;
`;
const HelloWorld = () => {
return (
<HelloWorldContainer>
<HelloWorldText>Hello World!</HelloWorldText>
</HelloWorldContainer>
);
};
export default HelloWorld;
The terminal output when running webpack-dev-server --config ./web/webpack.config.js --mode development
:
> [email protected] web /Users/shibli/repositories/member
> webpack-dev-server --config ./web/webpack.config.js --mode development
<i> [webpack-dev-server] Project is running at:
<i> [webpack-dev-server] Loopback: http://localhost:8080/
<i> [webpack-dev-server] On Your Network (IPv4): http://192.168.0.9:8080/
<i> [webpack-dev-server] On Your Network (IPv6): http://[fe80::1]:8080/
<i> [webpack-dev-server] Content not from webpack is served from '/Users/shibli/repositories/member/public' directory
<i> [webpack-dev-middleware] wait until bundle finished: /
asset vendor.ed39544ad714cff0e901.js 1.96 MiB [emitted] [immutable] (name: vendor) (id hint: commons)
asset main.9333044db6f8b6508335.js 54.2 KiB [emitted] [immutable] (name: main)
asset index.html 1.02 KiB [emitted]
Entrypoint main 2.01 MiB = vendor.ed39544ad714cff0e901.js 1.96 MiB main.9333044db6f8b6508335.js 54.2 KiB
runtime modules 31.2 KiB 15 modules
modules by path ./node_modules/ 1.58 MiB 460 modules
./index.js 1.62 KiB [built] [code generated]
./src/App.tsx 2.14 KiB [built] [code generated]
./app.json 57 bytes [built] [code generated]
webpack 5.90.3 compiled successfully in 21839 ms
<i> [webpack-dev-middleware] wait until bundle finished: /vendor.ed39544ad714cff0e901.js
<i> [webpack-dev-middleware] wait until bundle finished: /main.9333044db6f8b6508335.js
assets by status 1.96 MiB [cached] 1 asset
assets by info 55.1 KiB [immutable]
asset main.90bdf61fc277eb649d96.js 54.2 KiB [emitted] [immutable] (name: main)
asset main.3db06c133c551b7014f6.hot-update.js 849 bytes [emitted] [immutable] [hmr] (name: main)
asset main.3db06c133c551b7014f6.hot-update.json 28 bytes [emitted] [immutable] [hmr]
asset index.html 1.02 KiB [emitted]
Entrypoint main 2.01 MiB = vendor.ed39544ad714cff0e901.js 1.96 MiB main.90bdf61fc277eb649d96.js 54.2 KiB main.3db06c133c551b7014f6.hot-update.js 849 bytes
cached modules 1.58 MiB [cached] 463 modules
runtime modules 31.2 KiB 15 modules
webpack 5.90.3 compiled successfully in 72 ms
assets by path *.js 1.95 MiB
asset vendor.c259af1f674065728274.js 1.89 MiB [emitted] [immutable] (name: vendor) (id hint: commons)
asset main.6433fab5e93948076c82.js 52.8 KiB [emitted] [immutable] (name: main)
asset main.129784fe3c79354dbc8a.hot-update.js 3.74 KiB [emitted] [immutable] [hmr] (name: main)
asset index.html 1.02 KiB [emitted]
asset main.129784fe3c79354dbc8a.hot-update.json 419 bytes [emitted] [immutable] [hmr]
Entrypoint main 1.95 MiB = vendor.c259af1f674065728274.js 1.89 MiB main.6433fab5e93948076c82.js 52.8 KiB main.129784fe3c79354dbc8a.hot-update.js 3.74 KiB
cached modules 1.52 MiB [cached] 455 modules
runtime modules 30.6 KiB 12 modules
./src/App.tsx 1.9 KiB [built] [code generated]
webpack 5.90.3 compiled successfully in 106 ms
Then the page looks like it ignored styled-components:
But, if I change my App.tsx
to work with StyleSheet from react-native, it does apply the styling:
import React from 'react';
import { Dimensions, Text, View, StyleSheet } from 'react-native';
const { height, width } = Dimensions.get('window');
const HelloWorld = () => {
return (
<View style={styles.container}>
<Text style={styles.text}>Hello World!</Text>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
height: height,
width: width,
justifyContent: 'center',
alignItems: 'center',
},
text: {
fontSize: 58,
},
});
export default HelloWorld;
Why don't I go for it and change every single style?
I can't change all the styling files throughout the project, because there are more than 100 of them and this change could mess their funcitonality.
Edit 2
Here's my minimal reproducible example (kinda minimal, you just clone it):