Colleague’s feedback React Native project startup reported an error indicating version mismatch, the error screenshot is as follows.

err

After some troubleshooting, we finally found that the old version of js file was packed locally, and the project itself depends on a different version, so we can delete the old version of the local file.

We can find out that RN has a version check at startup through this error, how is the specific mechanism, let’s follow the source code together.

As for why an old js file is packaged locally and why it’s only a problem today after so many years, that’s a topic for another day, so ignore it for now

Version detection mechanism

Location of the error report

By searching for the keyword React Native version mismatch you can find the final code for the check in Libraries/Core/ReactNativeVersionCheck.js.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import Platform from '../Utilities/Platform';
const ReactNativeVersion = require('./ReactNativeVersion');

exports.checkVersions = function checkVersions(): void {
    const nativeVersion = Platform.constants.reactNativeVersion;
    if (
        ReactNativeVersion.version.major !== nativeVersion.major ||
        ReactNativeVersion.version.minor !== nativeVersion.minor
    ) {
        console.error(
            `React Native version mismatch.\n\nJavaScript version: ${_formatVersion(
        ReactNativeVersion.version,
      )}\n` +
`Native version: ${_formatVersion(nativeVersion)}\n\n` +
            'Make sure that you have rebuilt the native code. If the problem ' +
            'persists try clearing the Watchman and packager caches with ' +
            ' `watchman watch-del-all && react-native start --reset-cache` .',
        );
    }
};

This method compares the major and minor of ReactNativeVersion.version and Platform.constants.reactNativeVersion and throws an exception when the two values do not match.

If the version number is 0.59.9, then major is 59 and minor is 9. It feels like RN didn’t intend to remove the first 0. Meanwhile, checkVersion is loaded at startup, this part of the code you can search for yourself to see, not to analyze

ReactNativeVersion.version

So what do each of these two values represent? First, look at ReactNativeVersion.version, which is declared in Libraries/Core/ReactNativeVersion.js in the same directory as

1
2
3
4
5
6
exports.version = {
    major: 0,
    minor: 0,
    patch: 0,
    prerelease: null,
};

Well, it’s very clear and unambiguous. It’s like writing the same thing as not writing, no rush, we know anyway, the value is in the js file, will be with the final package into the bundle.js.

Platform.constants.reactNativeVersion

According to the reference, we find the file Libraries/Utilities/Platform.android.js with the following key content.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import NativePlatformConstantsAndroid from './NativePlatformConstantsAndroid';

const Platform = {

    ...

    get constants() {
            if (this.__constants == null) {
                this.__constants = NativePlatformConstantsAndroid.getConstants();
            }
            return this.__constants;
        }

        ...

};
module.exports = Platform;

is also directed to NativePlatformConstantsAndroid.getConstants(), in Libraries/Utilities/NativePlatformConstantsAndroid.js, as follows.

1
2
3
4
5
6
7
import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry';

...

export default (TurboModuleRegistry.getEnforcing < Spec > (
    'PlatformConstants',
): Spec);

Obtained via TurboModuleRegistry.getEnforcing('PlatformConstants'), continue down Libraries/TurboModule/TurboModuleRegistry.js.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
const NativeModules = require('../BatchedBridge/NativeModules');
const turboModuleProxy = global.__turboModuleProxy;

export function get < T: TurboModule > (name: string): ? T {
    if (!global.RN$Bridgeless) {
        // Backward compatibility layer during migration.
        const legacyModule = NativeModules[name];
        if (legacyModule != null) {
            return ((legacyModule: any): T);
        }
    }
    if (turboModuleProxy != null) {
        const module: ? T = turboModuleProxy(name);
        return module;
    }
    return null;
}

export function getEnforcing < T: TurboModule > (name: string): T {
    const module = get(name);
    return module;
}

A big pile of stuff, just one goal, to get a native module named PlatformConstants, that find this native module can reveal the secret, by searching PlatformConstants, you can find its native implementation in ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoModule.java, key code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Override
public @Nullable Map<String, Object> getConstants() {
    HashMap<String, Object> constants = new HashMap<>();

    ...

    constants.put("reactNativeVersion", ReactNativeVersion.VERSION);

    ...

    return constants;
}

Continue to ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.java in the same directory.

1
2
3
4
5
6
7
public class ReactNativeVersion {
    public static final Map<String, Object> VERSION = MapBuilder.<String, Object>of(
        "major", 0,
        "minor", 0,
        "patch", 0,
        "prerelease", null);
}

It’s the same as the js side, both are 0, which we’ll discuss later. As you can see, Platform.constants.reactNativeVersion is defined on the java side, and eventually in the native code, the com.facebook.react:react-native:0.59.9 we referenced in the build.gradle file contains this code. contains this code.

Phase Summary

As you can see, there is a version number on the js side and also a version number on the java side, both will be judged at startup and an error will be thrown if they are not the same.

The js and java are two dependencies, the js part is dependent in package.json and the java part is dependent in android/app/build.gradle, the two must match to work well, hence the above check work.

By analyzing the startup source code, we found that the check is actually performed only in the development environment, while the production environment does not have this section

Generally speaking, the development environment performs stricter checks than the production environment to ensure that development errors are exposed in a timely manner, while the production environment removes code that has nothing to do with the main function to ensure maximum efficiency at runtime. This can be said to be a means of handling most libraries, strict development and release, worth learning from

How to set the version number

If you look at the source code, you can see that the version number is 0. Then you can check the directory scripts/versiontemplates`, under which is the template for setting the version number, and the real operation is done in scripts/bump-oss-version.js#L60. This script takes a version number, fills in the previous template and overwrites the corresponding file in the project. This script is executed at release time, see step-2-cut-a-release-branch-and-push-to-github for details, and everything is clear up to that point.

So the version number is set up by script at release time, and is set up uniformly by way of templates to avoid manual changes and omissions

Template part is a simple replacement, did not refer to the additional template engine, can be simple to deal with will never get complicated, this is worth learning

A lot of scripts are written in js, so it is very easy to read and modify, we can also use js to deal with scripts, not to mention scripts on bash, python, in fact, js is also very popular

What happens when this error occurs

I encountered this case because the colleague used RN 0.55.4 to do manual packaging and uploaded the packaged js file to the repository, and then upgraded RN to 0.59.9.

From the previous source code analysis, if the wrong version of packager is started during development, it is also possible to generate this problem. We can understand that this mechanism is to ensure that the version of the js file downloaded from packager is the same for the currently running App, so that we can avoid continuing development on the wrong version, which will lead to the spread of the problem and not facilitate the troubleshooting of the final problem.

When we run into this problem, it is usually caused by the packager starting the wrong version. Secondly, unless you know what you are doing, it is strictly forbidden to generate js packages manually, and this part should be left to RN’s packaging scripts to execute and maintain, and cannot be submitted to the repository.

Why is there this check

I didn’t find the relevant instructions, but I can speculate. Personally, I think it is the RN development model that leads to the development stage, the computer will start a server, which is the packager mentioned above, used to distribute the latest js file, which is also the basis for the RN development stage can quickly update the code, because the distribution is independent, so this part is likely to happen version inconsistent problem, and version inconsistent is not affect most of the development, because the API is mostly compatible design, if you let this behavior, to the late development problems, troubleshooting will be very difficult. This is the basis for the rapid update of code in the development phase. And in the release phase, because all scripts are automatically executed, this part is relatively safe a lot.

Very often, some hard problems are caused by low-level errors, only the problems are hidden at the beginning and explode in the middle and late stages, when it is very time-consuming to troubleshoot them. Especially for open source projects like RN, it is a huge waste of resources if there are a lot of “hard problems” in issues that are caused by low-level errors. From this point of view, these basic tests are still necessary

Summary

In the development environment, the version numbers of both js and java are checked during the RN startup phase and matched before the actual system startup process begins. Adding this step of checking is to ensure the consistency of the development base environment and to ensure the development goes smoothly.

Also in the process of tracing the source code, you can learn a lot, including the design of the library, the differentiation between the development environment and the production environment, the template design, etc. For open source project errors, many times we can understand the nature of the problem through the source code, which is a great help to our development and learning.


Reference https://xiaomi-info.github.io/2020/01/02/react-native-version-check/