Search code examples
react-nativeexpocreate-react-native-app

Customizing code at build-time in React Native with Create React Native App and Expo


Generally, is there a way to customize the app code during build step?

Specifically, there is the typical need to make requests from the application to the local backend ([1], [2], [3], [4]).

  • localhost doesn't work since the server and the app are on different hosts (when using Android emulator or an actual physical device).
  • The actual IP address of the host in the same network works but in a multi-developer project it's a hassle for everyone to constantly change the constant IP in the code to that of their development machine.

With Webpack a case like that could be solved with DefinePlugin replacing a placeholder with the IP address of the machine the build is happening on.


Solution

  • We ended up using somewhat hacky approach inspired by transformers like react-native-typescript-transformer or react-native-sass-transformer. It's idea is pretty much equivalent to the mentioned DefinePlugin of Webpack.

    First, some transformer files in the project directory (you can name them however you like, just update the references):

    configBuildReplacements.js

    // whatever logic you need
    
    module.exports = {
        API_HOST_PLACEHOLDER: `http://${getLocalNetworkAddress()}:3000`,
        SOME_OTHER_DYNAMIC_VALUE: someFun(),
    }
    

    configBuildReplaceTransformer.js

    const semver = require('semver')    
    
    let upstreamTransformer = null
    
    const reactNativeVersionString = require('react-native/package.json').version
    const reactNativeMinorVersion = semver(reactNativeVersionString).minor
    
    if (reactNativeMinorVersion >= 56) {
        upstreamTransformer = require('metro/src/reactNativeTransformer')
    }
    else if (reactNativeMinorVersion >= 52) {
        upstreamTransformer = require('metro/src/transformer')
    }
    else if (reactNativeMinorVersion >= 47) {
        upstreamTransformer = require('metro-bundler/src/transformer')
    }
    else if (reactNativeMinorVersion === 46) {
        upstreamTransformer = require('metro-bundler/build/transformer')
    }
    else {
        // handle RN <= 0.45
        const oldUpstreamTransformer = require('react-native/packager/transformer')
        upstreamTransformer = {
            transform({ src, filename, options }) {
                return oldUpstreamTransformer.transform(src, filename, options)
            },
        }
    }
    
    
    module.exports.transform = function (src, filename, options) {
        // handle RN >= 0.46
        if (typeof src === 'object') {
            ({ src, filename, options } = src)
        }
    
        const replacements = require('./configBuildReplacements')
    
        const modifiedSrc = Object.keys(replacements).reduce(
            (src, replacementKey) => src.replace(
                new RegExp(replacementKey, 'g'),
                replacements[replacementKey],
            ),
            src,
        )
    
        return upstreamTransformer.transform({
            src: modifiedSrc,
            filename,
            options,
        })
    }
    

    The exported transform function uses the exported object from the previous file configBuildReplacements.js as a dictionary to replace key substrings with value substrings in the source code before handing this code to the default (upstream) transformer.

    And to connect this new transformer to the project:

    • with Expo, add the transformer packager option to app.json:

      {
        "expo": {
          "packagerOpts": {
            "transformer": "configBuildReplaceTransformer.js"
          }
        }
      }
      
    • without Expo, add getTransformModulePath() to rn-cli.config.js (which is the default path to the optional config file for React Native CLI, which is extremely poorly documented at the moment of this writing):

      module.exports = {
          getTransformModulePath() {
              return require.resolve('./configBuildReplaceTransformer')
          },        
      }
      

    After this is done, just like with DefinePlugin, code like

    get('API_HOST_PLACEHOLDER/info')
    

    will become something like

    get('http://192.168.42.23:3000/info')