How to survive breaking-changes of major React-Native release

Erez Zohar
ITNEXT
Published in
7 min readApr 14, 2020

--

React-native is the most popular cross-platform mobile framework, with more than +86k GitHub stars and over 363k active community developers.

TL;DR

A realistic assumption is that a new RN version contains breaking-changes, which is anew struggling for developers, causing unpredicted headache (!). Facing the upgrade dilemma, there’s a simple & efficient solution — find it inside :)

Knock` knock it’s the Major version

Recently we apprised a new major version release of react-native (0.62). Delightfully contains performance improvements and critical bug fixes that we all have been waiting for — hooray!

But before we let the excitement take over, we kind of know that when it comes to a new react-native major version release, then developers get into emergency situation 🤯 concerned with breaking-changes.

The BiG dilemma

You find yourself eager to upgrade to the new version, because of significant improvements and important bug fixes you waited for so long (i.e. users waiting for).

Then, on overviewing the version release-notes, the reality strikes you, realizing that the upgrading cost would be struggling with some other annoying 😡 breaking-changes.

This means that upgrading will force you to invest additional effort to reconcile unpredicted breaking-changes.

Well, the obvious question: is there any time left to allocate for, after app plans stamped?
and even more: do you really want to touch multiple code areas during this sensitive period? — it may require some bug fixing cycles, when resources are limited.

Let’s put the considerations on the wall:

If you choose to upgrade, then you probably find yourself invest further effort on reconciling discrepancies than the original roadmap, but if you won’t upgrade then you will find yourself leggy behind, carrying some continuous bugs and might be stuck with dependencies on new version…

So we face a “situation”: should we upgrade or not?

What is your call?

Breaking {through} changes

Before rush to any conclusions, let’s try to analyze the situation causes.

As noticed, the main issue here is the breaking-changes. If there were no breaking-changes, so the upgrade would have been smooth like a whiskey shot.

Most of the breaking-changes among react-native versions may be classified into 2:

  1. Component from react-native core, removed out to external repo, for community maintenance.
  2. Component API changed inside react-native core.

The wise among us, would say that there might be a 3rd option, where a Component internal functionality changed without API change. But actually it’s not a “breaking-change” but changing of behavior.

In order to reduce the risk of breaking-changes, we should find a way to reduce the friction between react-native API to our app code-base.

Reliefly, it doesn’t have to be a “blue pill red pill” situation, but there’s a light 😇 at the end of the tunnel.

[2] birds in (1) Wrap

Reducing friction between 2 code parts, also known as de-coupling and encapsulation, are fine principles of Object Oriented Programming.

De-coupling is ensuring that two different components are not tightly dependent on one another.

Encapsulation is hiding the inner functionality of a component behind a defined interface.

In our case, literally means to import and wrap a certain component from react-native package, and use this wrapped component as a source for all code-base.

Easiest as it sounds, here is a simple sample of wrapping TextInput component from the core of react-native:

  1. Create TextInput.js file in ./components/core dir

2. Import origin TextInput component directly from react-native package.

3. Encapsulate custom functionality of font-family style.

4. Consume TextInput from your local source.

Vualá, TextInput inner functionally is encapsulated inside the TextInput.js file, and can easily be controlled from a “single source of truth” without changing the consumed occurrences.

Follow this simple but efficient wrap, we facilitated isolation between react-native to the code-base, and enable easy customization for inner component functionality. 2 birds in 1 wrap — hooray!

Now then let’s see in action how to overcome breaking-changes follow this way.

Showcase study

Embrace yourself a habit, on a new release of react-native version, recommended to go over the release-notes — let’s check it out 🕵️ on the latest version 0.62.

Locate the section “Breaking” in the change-log details, i.e. “breaking-changes” — not so short…

React-Native v0.62 change-log, breaking-changes list

Observe the 2nd bullet: “Remove TextInput’s onTextInput prop”.
Sure we are all heavy consumers ofTextInput in our apps, imported directly from react-native package, may have occurrences of onTextInput within the app code-base. But as noticed, this API changed over the last major release. And sure we can overcome this breaking-change (*).

Commit details

1. Figure out 'onTextInput' functionality

onTextInput is a callback that is called on new text inputted by the user, triggers the same as onChangeText callback (I guess that’s why it removed from new react-native version due to redundant overlapped functionality).

The event data scheme is:

{
nativeEvent: {
text, // value of *diff* text
previousText, // the previous value of text
range: { start, end } // cursor position
}
}

What’s mainly interesting is the text value property.
Here’s how it used before the upgrade:
(pay attention that TextInput imported directly from react-native package)

2. Wrap

Create a new TextInput file component inside your ./components/core dir,
(I recommend to nest it under ./components/core which indicates its importance). Import TextInput directly from react-native package.

Aim all TextInput occurrences to the local source file created, instead of react-native directly — run the app via emulator just to make sure nothing breaks.

import TextInput from './components/core/TextInput';orimport { TextInput } from './components/core';

Now you gain a de-coupled isolated layer of TextInput component.

Right, we emphasized the wish to avoid wide code changes, so how come we apply such now? — actually we don’t, we don’t change any logic or functionality, but just the import reference. So there’s no risk on that move.

3. Encapsulate reconciliation

Implement an imitation of onTextInput callback inside the wrapper, in order to reconcile the forced breaking-change.

Follow the code instructions:

  • Implement a React component for TextInput . Remember to import React from 'react' , you need Babel to transpile the React component, otherwise all of this won’t work.
  • Divide onChangeText from the props and implement inner function — verify it’s been called properly.
  • “Ride” on onChangeText callback to trigger onTextInput event (because both of them same triggered, and under the new circumstances there’s no such callback onTextInput 🤓).
  • Fill onTextInput API to stand the schema:
{
nativeEvent: {
text, // value of *diff* text
previousText, // the previous value of text
range: { start, end } // cursor position
}
}
  • previousText — is actually the value property of TextInput which has not been changed yet (will commit after onChangeText ends).
const { value: previousText } = rest;
  • text — I used lodash to find the diff text, but you may use whatever you prefer.
import _ from ‘lodash’;
...
const { value: previousText } = rest;
const diiText = _.difference((newText || '').split(''), (previousText || '').split('')).join('');
  • range — taken from onSelectionChange callback which triggered on any cursor position change, I used Hooks here to save its state (**).
  • Make code more defensive to avoid errors in case some callbacks won’t be supplied as props — I used lodash but you may use whatever you prefer.
  • All together

4. Consume

This implementation is seamless for the consumer and functions the same as before without the need to change any logic or functionality in the code-base. Just consume it from the local source.

Wrap it all

Instead of competing with several modifications that may lead to long QA cycles, you have the right to protect yourself from the incoming breaking-changes.

This way of wrapping, considerably reduces the risk of oppressive breaking-changes reconciliation. Save time, money and nerves.

The recommendation: wrap it all.
You will have around ~40 lean ./components/core files, rather than spread broken occurrences all over the code-base.

Good luck with your next upgrade!

Find the above code sample @ my GitHub
Link to my Bio

(*) Proper disclosure: onTextInput will completely be removed on ver 0.62.2, still available on ver 0.62

(**) Not all onSelectionChangeedge cases covered in the article, it requires further long code sample which is out of this article scope.

--

--

Architect Lead with worldwide experience in scaling up systems and teams from scratch