I've been a react-native developer for about one and a half years now. I have only recently begun reading some of react-native-community/cli's source code in preparation of becoming an open source contributor. In the process of doing so, I have found ways of alleviating a few common pain points for me when developing with react-native. This gist will deal with one such pain point, as per the title above and is a part of a larger series of such posts.
Link to the github example project
Working at my last company, eventually we had to make modifications to the native code of some of the third party react-native libraries we used. We needed to share these modifications (of files under node_modules) to the entire react-native team. We resorted to using patch-package to share the aforementioned changes.
I started this project to see if I can make react-native load these third-party dependencies from a custom path for example (./modifiedRNLibraries) in the project root so that:
- The libraries we modify and use would be explicit (no scouring through .gitignore for the libraries we are actually commiting under node_modules or going through ./patches for the libraries modified with patch-package)
- We would have more control over version control for these libraries
At react-native's project root we add a file called react-native.config.js
that lists each (modified) third-party react-native library and the custom root you'd like to load each library from. A caveat to the root listed for each dependency name however is that the root needs to be an absolute path (details why below). For me to share the file with my team (can't use an hardcoded absolute path for that as our directory structures differ!), I simply have to convert the relative path to an absolute path dynamically using node's path
module. An example of one such react-native.config.js
file that declares a custom root for the react-native-image-with-progress-bar
third-party library is given below:
const path = require('path');
const relativePathToLibraryFromRNProjectRoot = "./customPath/react-native-image-with-progress-bar";
module.exports = {
dependencies: {
"react-native-image-with-progress-bar": {
root: path.resolve(__dirname, relativePathToLibraryFromRNProjectRoot)
}
},
project: { ios: {}, android: {} },
assets: [],
commands: [],
platforms: {}
};
Internally, the output of the react-native config
command is used by both cocoapod1 and gradle2 to obtain information about the third party libraries to link. If we look at the source code for the react-native config
command, we see that we can pass a custom root for a third-party react-native library using a react-native.config.js
file at the root of the react-native project 3.
If the root passed in via react-native.config.js
is an absolute path, like:
module.exports = {
dependencies: {
"react-native-image-with-progress-bar": {
root: "/Users/pirsanth/Documents/reactNativeProject/customPath/react-native-image-with-progress-bar"
}
},
project: { ios: {}, android: {} },
assets: [],
commands: [],
platforms: {}
};
we encounter no problems and the library from the custom root gets used when we run react-native run-android
or cd ios&&pod install&&cd ..&&react-native run-ios
(both from the RN project root). However we can't use an absolute path when we share the project amongst our work colleagues.
Using a relative path to the third-party library (relative to the RN project root) for root like:
//react-native.config.js at the RN project root
module.exports = {
dependencies: {
"react-native-image-with-progress-bar": {
root: "./customPath/react-native-image-with-progress-bar"
}
},
project: { ios: {}, android: {} },
assets: [],
commands: [],
platforms: {}
};
results in react-native-image-with-progress-bar
not being linked at all (both the native code from ./customPath and ./node_modules/ is not linked) when react-native run-android
is ran. This is because when you use a relative path for root in react-native.config.js the output of the react-native config
command changes based on the current working directory of the cli. When gradle runs the react-native config
command, the current working directory is set to the RN project's android source directory 4.
Thus, when we use a relative path to the third-party library relative to the android source directory like so:
//react-native.config.js at the RN project root
module.exports = {
dependencies: {
"react-native-image-with-progress-bar": {
root: "../customPath/react-native-image-with-progress-bar"
}
},
project: { ios: {}, android: {} },
assets: [],
commands: [],
platforms: {}
};
and we run react-native run-android
the library's native code in the custom path is used.
This approach works for android only: setting the relative path of the custom root of the third party library relative to the android source directory (./android in a normal react-native project). We will see in a later section how relative paths do not work as well for iOS.
Similar to the above case for android, using root: "./customPath/react-native-image-with-progress-bar"
(a relative path to the third-party RN library relative to the RN project root) results in the react-native-image-with-progress-bar
dependency not being added by cocoapods at all. This is because the current working directory when the react-native config
command is ran in native_modules.rb
1 is the ios source directory (./ios in a normal react-native project). Note: I found out the current working directory by copying the lines below into native_modules.rb
:
Pod::UI.puts "Dir.pwd #{Dir.pwd}"
IO.popen(["pwd"]) {|pwd_io|
pwd_result_with_error = pwd_io.read
Pod::UI.puts "pwd results #{pwd_result_with_error}"
}
However, unlike the android case above, using root: "../customPath/react-native-image-with-progress-bar"
(the relative path to the third party library from the ios source directory) results in an error when we run pod install
in the ios directory. This happens because of this line (in native_modules.rb
):
relative_path = podspec_dir_path.relative_path_from project_root
5
According to the Ruby Docs 6, in the above line the library's root (podspec_dir_path) and the project_root have to either both be relative paths or absolute paths. Furthermore, to accomodate the root of the library (podspec_dir_path) being a relative path, I will have to make both project_root and absolute_react_native_path relative paths as well due to 7.
Seeing as I'd have to modify so much to make the third-party library's root a hardcoded relative path on iOS, I decided to dynamically convert the relative paths to absolute paths as in the solution above to avoid unwanted side-effects. Besides, when there is no react-native.config.js
file at all react-native config
's output has all its paths as absolute paths by default.
If we change the root of a third-party library, we have to run pod install
in the ios directory before running react-native run-ios
. However for android, we can just run react-native run-android
straight away.
There are two types of react-native.config.js
files: the react-native.config.js
files placed at the root of a react-native project and the react-native.config.js
files placed at the root of a third-party react-native library (for example ./node_modules/react-native-image-with-progress-bar
). The format of the two files are as follows:
- The base structure for
react-native.config.js
at RN's project root:
{
dependencies: {},
project: { ios: {}, android: {} },
assets: [],
commands: [],
platforms: {}
}
- The base structure for
react-native.config.js
at the root of a RN third party library (either under node_modules by default or your custom path as discussed above):
{
dependency: {
platforms: { ios: {}, android: {} },
assets: [],
hooks: {},
params: []
},
commands: [],
platforms: {},
healthChecks: []
}
I think that this is best explained with an example. Keeping the directory structure of the example project in mind (pictured above with a link to its github), if we modify react-native.config.js
so as to use the ordinary (node_modules
version of the library without a red background) version of the react-native-image-with-progress-bar
library like so:
//the react-native.config.js file
//remember that we have to run `pod install` in the ios directory before running the `react-native run-ios` command
const path = require('path');
const relativePathToLibraryFromRNProjectRoot = "./modifiedRNLibraries/react-native-image-with-progress-bar";
module.exports = {
dependencies: {
// "react-native-image-with-progress-bar": {
// root: path.resolve(__dirname, relativePathToLibraryFromRNProjectRoot)
// }
},
project: { ios: {}, android: {} },
assets: [],
commands: [],
platforms: {}
};
What will happen when we run either the react-native run-ios
command or the react-native run-android
command is that the native code from ./node_modules/react-native-image-with-progress-bar
will be used, however the JS code from ./modifiedRNLibraries/react-native-image-with-progress-bar
is used instead of the JS code in ./node_modules/react-native-image-with-progress-bar
.
This was most surprising for the case where we do not specify a custom root for react-native-image-with-progress-bar
in react-native.config.js
as mentioned above but the same thing occurs when we do specify a custom root as well (by uncommenting the above snippet).
For ios when we run pod install
in the ios directory, the Podfile
has the line require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
which loads the native_modules.rb
file. The native_modules.rb
file, inturn, uses the result of the react-native config
command (which we can alter using the react-native.config.js
file as described above) to add the native code of the react-native-image-with-progress-bar
library (from either the custom root in ./modifiedRNLibraries/react-native-image-with-progress-bar
or the default root of ./node_modules/react-native-image-with-progress-bar
) via cocoapods
For android when we run the react-native run-android
command the gradle wrapper's task that is ran uses the native_modules.gradle
file to similarly run the react-native config
command and add the native code of the react-native-image-with-progress-bar
library (from either the custom root in ./modifiedRNLibraries/react-native-image-with-progress-bar
or the default root of ./node_modules/react-native-image-with-progress-bar
) via gradle.
However, on the JS side of things, babel will transpile the code in ./App.js
and when the line corresponding to import ImageWithProgressBar from 'react-native-image-with-progress-bar';
in ./App.js
is ran, the JS module resolution will occur. I don't know what module resolution algorithm react-native uses but it isn't the same as node's require()
8. What we can infer from the above experiment with the example project though is that the JS module resolution algorithm seems to be unaffected by the changes in react-native.config.js
. I might update this in the future as I read more of the source code and understand the module resolution algorithm better.
- How cocoapods uses react-native config's output
- How gradle uses react-native config's output
- How react-native config uses the information read from the react-native.config.js file at RN's project root to define a custom root to a third-party library
- Gradle runs the react-native config command with the cli's current working directory set to the RN project's android source directory
- Why pod install in the ios directory throws an error when a relative path is used for the RN third-party library's root
- Ruby Docs for Pathname's relative_path_from method
- Why we need to make absolute_react_native_path a relative path if we've made project_root a relative path
- Ben Nadal on how node's require looks for js modules