2019-03-30

Spread syntax gotcha in JavaScript class methods

javascript, typescript, react, hooks

banner

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.

https://gist.github.com/dance2die/3107d7b0a3d3eeeb0dcad5886f5b1bf9

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

https://gist.github.com/dance2die/4192b1bab9e70e046515b456c147baf8

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).

https://gist.github.com/dance2die/7419342babb86cafb72aaae443cff2c2

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 [prototype](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/prototype) is not enumerable so has is not copied over.

πŸ§™β€β™‚οΈ Resolving the Issue

You can resolve the issue in two ways.

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

1. Bind Explicitly

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

https://gist.github.com/dance2die/0e123ece5776d334bf5e7fe39d88410b

, which is TypeScript-transpiled as

https://gist.github.com/dance2die/8e5597304ea3246396d1c40506169736

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

https://gist.github.com/dance2die/95b8a2bbf71316895673148e56441f18

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.

https://gist.github.com/dance2die/5c1a5a0252c3851fc22bb95f3da0b117

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.

https://gist.github.com/dance2die/0d5a69a187ab07e9234bad36dbe72cad

πŸ€¦β€β™‚οΈ 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.

https://gist.github.com/dance2die/13306ec6ab90419382e933434a131ad5

FYI - [useTrie](https://github.com/cshooks/hooks/blob/master/packages/useTrie/src/index.ts#L218) is implemented as shown below.

https://gist.github.com/dance2die/043283084a0ae76b7a8bef2c86799fb6

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.

https://twitter.com/PaulGrizzay/status/1105941010344038401

https://twitter.com/dan_abramov/status/1105946933955301377

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.