DEV Community

Cover image for Spread syntax gotcha in JavaScript class methods
Sung M. Kim
Sung M. Kim

Posted on • Originally published at slightedgecoder.com on

5 1

Spread syntax gotcha in JavaScript class methods

Photo by Jay on Unsplash – Don’t get caught

Watch out when exposing a mutable data structure with React Hooks

When you spread an object instance of a class to expose methods, methods might not be copied over.

Suppose that you have a Trie class,

which you want to make it immutable by returning a new object using syntax spread.

Not a good idea! Explained later.

class Trie {
has(word) { return true; }
}
function useTrie() {
const trie = React.useState(new Trie())[0];
return { ...trie };
}
view raw opening.Trie.js hosted with ❤ by GitHub

Printing out trie object instance returned from useTrie won’t show has and an empty method is printed.

function App() {
const trie = useTrie();
console.log(`{...trie}`, { ...trie });
return <div className="App">Nothing to see here</div>;
}
// console shows
{...trie} Object {}
view raw opening.App.js hosted with ❤ by GitHub

Let’s see why and how to solve the issue.

🔬 Analysis

To understand the problem, let’s see how the class is transpiled using TypeScript compiler (the transpiled babel code does the same but verbose so using TypeScript compiler here).

class Trie {
has(word) { return true; }
}
// Transpiled as
var Trie = /** @class */ (function () {
function Trie() {
}
Trie.prototype.has = function (word) { return true; };
return Trie;
}());

has method was added to the prototype, not to an instance of Trie class.

So has is still available when you do const t = new Trie(); t.has(); // true.

Returning a new object using spread syntax didn’t copy has

because spread syntax only copies own & enumerable properties.

But prototypeis not enumerable so has is not copied over.

🧙‍♂️ Resolving the Issue

You can resolve the issue in two ways.

  1. Bindingthe method explicitly to this.
  2. Using an arrow function expression.

1. Bind Explicitly

You can explicitly bind this to the method in the constructor.

class Trie {
constructor() {
this.has = this.has.bind(this);
}
has(word) { return true; }
}
view raw bound Trie.js hosted with ❤ by GitHub

, which is TypeScript-transpiled as

var Trie = /** @class */ (function () {
function Trie() {
this.has = this.has.bind(this);
}
Trie.prototype.has = function (word) { return true; };
return Trie;
}());

And printing the trie instance returned from useTrie will now show .has method.

{...trie}
Object {has: function bound has()}

has is still added to the prototype, which might not be what you want and it’s increasing the file size.

So this brings us to,

2. Using an arrow function expression

When you declare the has method using an arrow syntax, it’s transpiled by Transcript as shown below.

class TrieUsingArrow {
has = word => true;
}
// Transpiled by TypeScript as
var TrieUsingArrow = /** @class */ (function () {
function TrieUsingArrow() {
this.has = function (word) { return true; };
}
return TrieUsingArrow;
}());

You can see that it’s same without has being assigned to the prototype.

And the console log will still show has as part of the trie instance returned from useTrieUsingArrow.

{...trieUsingArrow}
Object {has: function ()}

🤦‍♂️ Why? Why? Why?

I recently released a new package @cshooks/usetrie and Nick Taylor

generously provided an educational & thorough PR on how the code-base can be improved.

But not having a deep knowledge of TypeScript & Javascript, the following change caused an issue.

-has = (wordToSearch: string, exactSearch: boolean = true): boolean => {
+has(wordToSearch: string, exactSearch: boolean = true): boolean {

FYI – useTrie is implemented as shown below.

function useTrie(
initialWords: Word[],
isCaseInsensitive = true,
getText: TextSelector = obj => obj
): ITrie {
const trie = new Trie(initialWords, isCaseInsensitive, getText);
const [state, dispatch] = React.useReducer(reducer, { trie, word: '' });
// add/remove removed for brivity
return { ...state.trie, add, remove };
}

I was retro-fitting a mutable data structure and exposing it as a hook.

But it’s not a good way as you can see between Paul Gray & Dan Abramov‘s tweets.


So be aware of the issue discussed above when you are extracting an imperative logic out of React.

🎉 Parting Words

I’ve paid handsomely for not following React way of doing things.

I hope you the gotcha & the workaround helped you understand what’s going on behind the scenes.

You can play around with the TypeScript transpiler on the Playground page
& the console log results in the Sandbox.

The post Spread syntax gotcha in JavaScript class methods appeared first on Sung's Technical Blog.

Sentry blog image

How I fixed 20 seconds of lag for every user in just 20 minutes.

Our AI agent was running 10-20 seconds slower than it should, impacting both our own developers and our early adopters. See how I used Sentry Profiling to fix it in record time.

Read more

Top comments (2)

Collapse
 
nickytonline profile image
Nick Taylor

Thanks for the shoutout @dance2die . Happy I could help.

Collapse
 
dance2die profile image
Sung M. Kim

You're welcome & I could not have learned about this without your help 🙂

Billboard image

Create up to 10 Postgres Databases on Neon's free plan.

If you're starting a new project, Neon has got your databases covered. No credit cards. No trials. No getting in your way.

Try Neon for Free →

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay