Search code examples
javascriptreactjsreact-nativewebpackmetro-bundler

Using appropriate transpilers for React Native Web Project


I am trying to create a React Native Web project.

I have built several React Native apps before, but have never tried to put one on the web.

My biggest problem has been incompatibility between native libraries when launching the web - not an unexpected problem.

Anyway, my goal is to be able to load native libraries when on a native platform and having alternative libraries doing the same thing when on the web.

For example, I am getting the current error:

./node_modules/react-native-calendars/src/expandableCalendar/asCalendarConsumer.js
Module parse failed: Unexpected token (11:8)
You may need an appropriate loader to handle this file type.
|     render() {
|       return (
|         <CalendarContext.Consumer>
|           {(context) => (
|             <WrappedComponent

How would I fix this? This library is theoretically compatible with React Native Web, and yet I get the above error.

Would this loader be in Babel? Metro? Webpack?

I have a babel.config.js that looks like this:

module.exports = {
  presets: ['module:metro-react-native-babel-preset'],
  resolve: {
    alias: {
      'react-native$': 'react-native-web'
    }
  },
  rules: [
    {
      test: /\.(js|jsx|mjs)$/,
      include: [
        paths.src,
        // In order to use react-native targetted libraries on web,
        // we have to use babel to compile them from ES6 to ES5.
        // This would still not allow us to use libraries that have RN
        // dependencies that are not polyfilled by react-native-web.
        path.resolve(paths.nodeModules, 'react-native-vector-icons'),
      ],
      loader: 'babel-loader',
      options: {
        compact: true,
        presets: ['react-native'],
      },
    }
  ]
};

I have a metro that looks like this:

const { getDefaultConfig } = require("metro-config");

module.exports = (async () => {
  const {
    resolver: { sourceExts }
  } = await getDefaultConfig();
  return {
    transformer: {
      babelTransformerPath: require.resolve("react-native-css-transformer")
    },
    resolver: {
      sourceExts: [...sourceExts, "css"]
    }
  };
})();

And here is my webpack:

// webpack.config.js
module.exports = {
    plugins: ["@babel/plugin-syntax-dynamic-import"],

    resolve: {
        alias: {
            'react-native$': 'react-native-web'
        },
    },
    rules: [
        {
            test: /\.js$/,
            loader: 'babel-loader',
            //exclude: /node_modules/,
            options: {
                presets: ['es2015', 'stage-0', 'react', 'babel-preset-env', 'babel-preset-stage-0'],
                plugins: ["@babel/plugin-syntax-dynamic-import"],
            }
        },
        {
            test: /\.ttf$/,
            loader: "url-loader", // or directly file-loader
            include: path.resolve(__dirname, "node_modules/react-native-vector-icons"),
        },
    ]
}

I'm really quite lost on how to setup a Webpack, or how I am supposed to be using these files to get rid of the above error.

Where do I add the loader the error is asking about?

Sorry if this is a confusing question - this part of RN is completely new to me


Solution

  • Actually, I faced an issue like it but not the web, our project needs to have a unique logic but different UIs for Android and iOS, so we decide to decoupled the UIs with different files by .android.js and .ios.js files, its name is Platform-specific extensions and also config it on the .babelrc file:

    {
      "presets": ["module:metro-react-native-babel-preset", "module:react-native-dotenv"],
      "plugins": [
        "lodash",
        ["module-resolver", {
          "extensions": [".android.js", ".ios.js", ".js"], // here
          "cwd": "babelrc",
          "root": ["./app"]
        }]
      ],
      "env":{
        "production":{
          "plugins": ["transform-remove-console"]
        }
      }
    }
    

    So for decoupling the UI for each stack be like below:

    CheckoutPage.android.js
    CheckoutPage.ios.js
    

    For importing the component we use this way:

    import Checkout from '[pathToComponent]/CheckoutPage';
    
    ~~~
    
    <CheckoutPage ...
    

    Solution:

    Now my suggestion is using another file extension, the web.js and put it in the babel configuration:

    "extensions": [".android.js", ".ios.js", ".web.js", ".js"],
    

    Furthermore, add the web.js extension to the Webpack configuration for loading in the web build and use ignore-loader to ignore .ios.js and .android.js files on the web build:

    // webpack configuration file
    
    module.exports = {
    
      module: {
        rules: [
          {
            test: /\.(js|web.js)$/,
            exclude: /node_modules/,
            use: {
              loader: "babel-loader",
            },
          },
          {
            test: /\.(android.js|ios.js)$/,
            exclude: /node_modules/,
            use: {
              loader: "ignore-loader",
            },
          },
        ],
      },
    
      ~~~
    
      resolve: {
        extensions: ['.web.js', '.js'],
      },
    };
    
    

    For a better explanation, I create a project on CodeSandBox, and you can see there I just call import Home from './Home'; but the Home.web.js component is rendered.

    By using this trick you can use platform-specific extensions even on the web build or develop.