How to survive breaking-changes of major React-Native release
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:
- Component from
react-native
core, removed out to external repo, for community maintenance. - 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
:
- 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…
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 (*).
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 forTextInput
. Remember toimport React from 'react'
, you needBabel
to transpile theReact
component, otherwise all of this won’t work.
- Divide
onChangeText
from theprops
and implement inner function — verify it’s been called properly.
- “Ride” on
onChangeText
callback to triggeronTextInput
event (because both of them same triggered, and under the new circumstances there’s no such callbackonTextInput
🤓).
- 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 thevalue
property ofTextInput
which has not been changed yet (will commit afteronChangeText
ends).
const { value: previousText } = rest;
text
— I usedlodash
to find the difftext
, but you may use whatever you prefer.
import _ from ‘lodash’;
...const { value: previousText } = rest;
const diiText = _.difference((newText || '').split(''), (previousText || '').split('')).join('');
range
— taken fromonSelectionChange
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 usedlodash
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!
(*) Proper disclosure: onTextInput
will completely be removed on ver 0.62.2, still available on ver 0.62
(**) Not all onSelectionChange
edge cases covered in the article, it requires further long code sample which is out of this article scope.