Repository navigation
Depending on Expo modules in other packages #13963
Replies: 1 comment 3 replies
|
Just to expand on the optional dependencies part. There actually is a way to declare optional peer dependencies in npm, using the If the packages is actually optional (that is, the package will work without the dependency) I don't think that there is problem with doing a try/catch import of it. But in that case I think that there is a benefit of specifying that as an optional peer dependency so that npm can still now about the relation so that it tries to move the dependencies to the right place in the dependency graph so that it can be imported. Now, in the case of Sentry/Bugsnag, I don't think that optional dependencies is all that great in this case. It's just pushing the error further down, when all metadata shows up as I think optional dependencies is better suited when some functionally is not available on a specific platform, which is just a subset of the platform being supported by that package. That being said, I was not aware of the warnings that expo prints. I think that this is great, but I would personally prefer it if they were actual errors (which could have a flag to ignore them and turn them into warnings). If we have that, and then make sure that the ecosystem is using peer dependencies, I think that we would have largely solved this issue. To reiterate from the thread that we had which sparked this discussion, I think that a huge drawback with not declaring any dependencies and then importing them anyways is that it's very easy to forget why dependencies was added, and it's easy to accidentally remove them in the future. It basically moves the responsibility of keeping track of dependencies from npm to the end user. As an example, you have a project where you are already using Now you are doing changes to your app and you remove your code that used In fact, if you fix this by adding back The more I think about this I think that the best approach is to use peer dependencies in the libraries, and let expo error out if you have the wrong version of the package anywhere in your dependency graph. The reason for using peer dependencies instead of normal dependencies in the libraries is because there can only be exactly one version of this packages at the same time, and using a normal dependency will allow npm to add that dependency deeper in the graph to satisfy different requirements... This was just a quick brain dump that became quite long 😅 I'm very happy to continue the discussion and very open to all arguments for different solutions! |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
It's common to want to depend on Expo modules from other npm packages. While certainly not the only case where this applies, you will find this in error reporting libraries (eg: bugsnag, sentry) and analytics libraries (eg: segment, firebase), because they often want to automatically add some context to events, such as the device type, operating system version, app version, etc.
React Native doesn't expose any core primitives to access this information, so these libraries need to depend on either their own native code (traditionally not supported in managed apps, although it is supported now with custom dev clients) or another native library that exposes an interface to the desired data. For example: expo-device, expo-application, expo-constants, expo-updates. So, these libraries add dependencies on packages like expo-device.
Not really though, jk.
Problems with depending on Expo SDK packages
eg: expo-device@~3.3.0 works with Expo SDK 42 / react-native-unimodules 0.14.6
In Expo Go, this is a hard requirement because you don't control the native runtime. In EAS Build, there is some flexibility, but Expo SDK packages are built alongside react-native-unimodules (soon to be expo-modules-core) and there may be some compatibility issues if the versions are not aligned.
The result is that you will need to have releases of your package for every Expo SDK version to ensure the dependencies line up properly with those compatible with specific Expo SDKs. You'll need to update it each time a new Expo SDK is released. There also is not yet a way for third party packages to hook into
expo installandexpo upgradeto ensure the correct version is installed for the project (this is something that we do intend to support in the future). But overall, depending on specific package versions is not great.Subproblem: npm is not designed to handle native dependencies
To further complicate things, npm does not do so well with the constraints imposed by native dependencies. With JavaScript dependencies, it's often okay (not always, sometimes this is problematic due to package level statefulness) to just install whatever version of a package your library needs, and if another library needs another version of that package, that's not a problem. Each one can import the version they need, everyone is happy. This is problematic for native dependencies in React Native because you can only link one module at a time, and then the JS code for all instances of that package in your node_modules will run against that same version of the native interface, which can result in hard crashes on app launch (eg: if it's a view manager), obscure errors (eg: if interface changed in a way that doesn't crash but returns different results), and all sorts of other problems.
What about peer dependencies?
Cool, so maybe we can just avoid this by using peer dependencies? We can say that this library requires a peer dependency
"expo-device": "*"or"expo-device": "~3.3.0 || ~4.0.0"and then the user will be warned that they need to install this library, right? Right?No. The behavior of peer dependencies changed in npm 7, so now it will be automatically installed if needed, rather than just warn.
If we depend on
"expo-device": "*"then npm 7 will install the latest version of expo-device, rather than the version used by the project's Expo SDK (because npm doesn't and shouldn't know anything about Expo SDK versions). If this installed an incompatible version of expo-device, users would be warned inexpo startorexpo doctor:This is not the worst result, and it is what is done in sentry-expo, but it could still result in users having broken apps if they ignore these warnings. This can be mitigated by advising users to install the peer dependencies as dependencies along with the package installation instructions:
Now this ensures the correct version will be used and they will be upgraded with
expo upgradeto continue working in the future.If we use
"expo-device": "~3.3.0 || ~4.0.0"for our peer dependency we would possibly to update the package for every new SDK version, or whenever a version is released that has a breaking change that requires a version bump that falls out of the compatible range, even if the functionality depended on does not break. If we use"expo-device": "*", we may end up depending on an API that has changed or no longer exists. Both of these appear to be legitimate options depending on the preferences of the library maintainer, a decision that they can make based on the expected failure mode.What about optional dependencies?
The npm idea of "optional dependencies" doesn't work here, but using
try/catcharoundrequireallows us to conditionally use a library if it is available.Library authors can then include the package in their
devDependenciesbut leave it out of peer and regular dependencies.@LinusU is not a big fan of this approach:
What about dependency injection?
Libraries like redux-persist have solved similar problems by requiring that the developer pass in the API in the library configuration:
This is another possible solution in some cases. It has the benefit of being very explicit, and also gives users the ability to swap in their own implementations, provided they conform to the expected interface. The downside is that it no longer "Just Works".
This thread is intended to be an open discussion about this problem, please share any thoughts or opinions.
All reactions