Implementing the Matrix JS SDK in React Native

3 minute read

Meet Kinksters’ chat feature is backed by Matrix, an awesome open-source communication protocol and its Synapse homeserver reference implementation.

In our React Native app, we connect with the excellent matrix-js-sdk library (which is also behind their official Element client).

All of the Matrix ecosystem is under rapid development but is production-ready. When using the SDK on React Native, however, there are a few gotchas worth documenting to save others some “WTF” moments. It’s worth noting that none of these pain points are the fault of the SDK maintainers but stem instead from limitations and implementation details in React Native itself.

Incomplete Intl support in React Native Hermes runtime

Hermes is a new-ish JavaScript engine for React Native that yields performance and DX benefits. However, it lacks support for the Intl spec on iOS in a few key areas. Due to this change in matrix-js-sdk, the Metro bundler needs to find Intl.Collator, which it can’t on iOS.

There are various code snippets around the web on pollyfill-ing Intl in RN, but many of these solutions do not support Collator. I spent more time than I should have trying to implement various elegant fixes, however given the library’s narrow (single) use of this feature, I settled on the only solution that seemed to work for me - including a homegrown polyfill.

metro.config.js

// Learn more https://docs.expo.io/guides/customizing-metro
const { getDefaultConfig } = require('expo/metro-config');

const config = getDefaultConfig(__dirname);

const originalPolyfills = config.serializer.getPolyfills;
config.serializer.getPolyfills = ({platform}) => {
  let polyfills = originalPolyfills({platform});
  if (platform === 'ios') {
    polyfills = polyfills.concat([__dirname + '/intl.polyfill.js']);
  }
  return polyfills;
};

module.exports = config;

intl.polyfill.js

if (typeof global.Intl === 'undefined') {
  global.Intl = {};
  global.Intl.Collator = function () {}
  global.Intl.Collator.prototype.compare = (a, b) => a.localeCompare(b);
}

My comfort with this patch is buoyed by the fact React Native 0.70.0 includes the missing support. Expo hasn’t yet rolled an SDK including this version, but I can rip this out when they do.

Missing WebCrypto support in React Native

This change in the SDK now finds a WebCrypto implementation from the browser or Node 16+. The issue of course for React Native is that we’re neither in a browser nor running on Node.

Support for WebCrypto API in React Native seems unlikely to land officially and the one polyfill I have found only supports Android. So, this is a limitation to consider if implementing E2E encryption on RN with Matrix.

Our current chat implementation is not end-to-end encrypted (in order to facilitate moderation; this might change in the future) so this isn’t a major restriction, however the SDK and Metro bundler still expect to find some crypto functionality when building.

The same author of the full polyfill has, however, provided an actively-maintained polyfill for just crypto.getRandomValues, which is enough to satisfy Metro.

I’ve opened an issue to perhaps more gracefully handle a missing WebCrypto API.

metro.config.js

// Learn more https://docs.expo.io/guides/customizing-metro
const { getDefaultConfig } = require('expo/metro-config');

const { resolver } = getDefaultConfig(__dirname);

config.resolver = {
  ...resolver,
  extraNodeModules: {crypto: require.resolve('react-native-get-random-values')},
};

module.exports = config;

URL is broken and won’t be fixed in React Native

Just a few days ago, the SDK updated its HTTP client from browser-request to fetch. This is super helpful to me, since request is unmaintained and lacked proper middleware support. So, I was having to implement a homebrew polyfill that delegated to fetch under the hood. I knew it was janky but lacked the expertise and time to provide an upstream refactor.

The awesome people at Matrix did just that, and it landed just a few days ago. This did mean some non-trivial refactoring on my side, but I was happy to do so as it meant reducing my technical debt and being able to more easily keep pace with their upstream changes.

After making what I thought were all the required changes, I found that Matrix was generating URLs with trailing slashes added, such as /_matrix/client/versions/ to which Synapse would reply with {errcode: "M_UNRECOGNIZED", ...}.

It took a bit of sleuthing to find the culprit, which was URL(). Turns out it’s broken in React Native and in such a way that fixing would be a BC break so the semi-official answer is to use a polyfill, forever.

TL;dr: if you have an RN project, include react-native-url-polyfill as a matter of course to avoid chasing your tail.

Updated: