React: Where To Extend Object Prototype
TLDR Answer: Nowhere!
-- Nuanced Answer --
What I'm trying to do is extend pure JavaScript classes, like String class, which is a very common task in javascript
Is it even "OK" to extend object prototype in React (or in JavaScript) at all?
Extending/modifying native prototypes in JavaScript is a controversial topic, and, contrary to what you said, not something most professional developers do very often. The general consensus is that extending the native JS prototypes is a programming anti-pattern to be avoided, because it breaks the principle of encapsulation and modifies global state. However, as with many rules, there may be rare exceptions to it. For instance: you're working on a toy project that doesn't need to be production quality, you're the only dev who will ever touch that code base, or your code will never be a dependency for anyone else.
If you have a really good reason and really know what you're doing and are fully aware of the potential consequences of your modifications to the native data types/behaviors for your run-time environment and dependencies, then perhaps you will find some valid use case for this practice. But most likely not, or at least not very often. Like almost never.
If you're just after convenience / syntactic sugar, you're better off pulling in utility functions (from the likes of lodash, underscore, or ramda) and learning to practice functional composition. But if you're really committed to the Object Oriented paradigm, then you should probably just be "subclassing" the native data types rather than modifying them.
So rather than mutating a class's prototype like this:
String.prototype.somethingStupid = function () {
return [].map.call(this, function(letter) {
if ((Math.random() * 1) > .5) return letter.toUpperCase()
else return letter.toLowerCase()
}).join('')
}
console.log('This is a silly string'.somethingStupid())
You would create a sub-class (only works with ES6 class syntax), like so:
class MyString extends String {
constructor(x = '') {
super(x)
this.otherInstanceProp = ':)'
}
somethingStupid() {
return [].map.call(this, function(letter) {
if ((Math.random() * 1) > .5) return letter.toUpperCase()
else return letter.toLowerCase()
}).join('')
}
}
const myStr = new MyString('This is a silly string')
console.log(myStr)
console.log(myStr.valueOf())
console.log(myStr.somethingStupid() + ', don\'t you think?')
This subclass would work like a built-in String in every way, except of course that you wouldn't be able to write MyString literals like String literals.
I created a pure React application using create-react-app. I would like to extend the String class and use it in one or more components ... Yes I can define it beside a component and use it within. But what is the best and cleanest way? ... Should I write it as a class method or inside componentDidMount or something else?
Because modifying built-in prototypes (by mutating things like String.prototype
) alters the global state of your application, it is something that you will want to execute only once and almost certainly before any other code executes (because you're setting the global state of how Strings behave for all code that executes after). So altering built-in prototypes from within a React component instance method wouldn't make much sense.
If you're going to do the dirty deed, I'd recommend creating a separate module for each native type you want to modify, and keep those modules somewhere like src/lib/extend-built-ins/
or something, and then import
them as the very first thing in src/index.js
. You wouldn't need to export anything. Doing import src/lib/extend-built-ins/String.js
will execute the code, which will mutate your global state. That would provide at least decent organization and ensure that your application environment is fully modified before the rest of your app's code runs. That way you can just use your extended types throughout your application without thinking about importing them from somewhere.
If you're going to go the subclassing route (class MyThing extends NativeThing
), then I would recommend you similarly define your custom classes in separate modules somewhere like src/lib/native-subclasses/
. But in this case, you would have to import
your class constructors into any/every module where you want to use them.
However, if you want to develop clean, testable, refactorable code that will be easy for others and your future self to understand, you shouldn't do this sort of thing. Instead, think about adopting the functional programming principles of React and its ecosystem. Anyone can quickly read and understand a pure function, so use them to accomplish your data and state transformations rather than relying on hard-to-track hacks like modifying global objects. It may be cute and trivial to understand this one little hack, but doing it even once in a project promotes and encourages yourself and others to use additional shortcuts and anti-patterns.
For TypeScript Language Users
Tested on javascript based react-native project using expo-cli 36 and was wrote while working on Angular 7
I had error that brought me here... I'm using js based react-native. previously i wrote a JS library, then due to some needs, i changed it to TS, and after switching to Angular, i had to create a firm library out of it to work in that environment...
No i copied these file here, and i had error that i came to this thread, but all error gone once i compiled it for the first time, and it worked fully...
Here's how i used it:
i have utility class Utility.ts
:
export class Utility {
/**
* Returns False If Incoming Variable Is Not Null, void or Undefined, Otherwise True
*/
public static isNullOrUndefined(obj: any|void): boolean {
// return obj == null // juggling-check
return typeof obj === 'undefined' || obj === null; // strict-check
}
/**
* Returns False If Incoming Variable Does Not Contains Data, Otherwise True
*/
public static isNullOrUndefinedOrEmpty(obj: any|void): boolean {
if (Utility.isNullOrUndefined(obj)) {
return true;
}
// typeof for primitive string, instanceof for objective string
if (typeof(obj) === 'string' || obj instanceof String) {
return (obj as string).valueOf() === '';
} else if (obj instanceof Array) {
return (obj as Array<any>).length === 0;
}
throw new Error('Not Supported Exception');
}
...
}
I have a definition class index.d.ts
(I'm not sure but i think the file name was super important at Angular time, feel free to test...):
declare global {
interface StringConstructor { // for static methods
format: (str: string, ...args: any[]) => string;
}
interface String { // for instance methods
/**
* Support StringComparision Options
* https://stackoverflow.com/questions/10636871/ordinal-string-compare-in-javascript
* @param searchString {!string}
* @param position {?number}
* @param comparision {?StringComparison}
* @returns {number}
*/
indexOfString: (searchString: string, position?: number, comparision?: StringComparison) => number;
insertAt: (index: number, strText: string) => string;
removeAt: (index: number, count: number) => string;
replaceAt: (index: number, count: number, strReplacement: string) => string;
overrideAt: (index: number, strText: string) => string;
...
}
And in the end i have my extension files Extensions.ts
:
import { Utility } from './Utility';
/**
* Support StringComparision Options
* https://stackoverflow.com/questions/10636871/ordinal-string-compare-in-javascript
*/
String.prototype.indexOfString = function(searchString: string, position?: number, comparision?: StringComparison): number {
return Utility.indexOfString(this, searchString, position, comparision);
};
String.prototype.insertAt = function(index: number, strText: string): string {
return this.substr(0, index) + strText + this.substr(index);
};
String.prototype.removeAt = function(index: number, count: number): string {
if (Utility.isNullOrUndefined(count)) {
count = this.length - index;
}
return this.substr(0, index) + this.substr(index + count);
};
Here i put all these files in a folder named util
which doesn't matter at all, and i have direct access from Extensions to Utility and vise versa (or i didn't had any issue till now.
NOW although i still can't use my extensions in react component, i can if i add a simple import: import '../util/Extensions';
I tested it like this:
import React from 'react';
import { Text, View } from 'react-native';
import '../util/Extensions'; //my import
const SurveyScreen = props => {
const z = 'sdwqew';
const x = z.insertAt(2,'hassan'); //function from my custom extensions
return (
<View>
<Text>Hello World</Text>
<Text>{x}</Text>
</View>
);
}
export default SurveyScreen;
Note at this time i do not have enough time to fully overview my old code, but here's the thing
Second Note: if i import Extensions in Utility, it gives me warning Require cycles are allowed, but can result in uninitialized values. Consider refactoring to remove the need for a cycle. if i don't i see some error on VSCode, but it compiles fine, with no warning...