DEV Community

Kenneth Lum
Kenneth Lum

Posted on

Whenever we see (d) => setData(d), what can we think about?

Sometimes we have some code:

fetch(" ... ")
  .then(res => res.json())
  .then(d => setData(d))
  .catch(err => console.error(err));
Enter fullscreen mode Exit fullscreen mode

Whenever we see the form

(value) => doSomething(value)
Enter fullscreen mode Exit fullscreen mode

that is, get X, and do something with X, then we actually can change it to

doSomething
Enter fullscreen mode Exit fullscreen mode

So the original code above, can be

fetch(" ... ")
  .then(res => res.json())
  .then(setData)
  .catch(console.error);
Enter fullscreen mode Exit fullscreen mode

and internally, we can convert .then(setData) to .then(v => setData(v))

The exception is when doSomething can take in more than one argument, but x => doSomething(x) will be sure to only get the first argument.

This pattern works if it is the .then(setData) case, where it takes a function that accepts one argument only.

Now, note that for the case of .then(console.log), it works, because log as a function doesn't need to have the this bound to console. This is very subtle. When we provide console.log to .then(), we have the "lost binding" in JavaScript. So when .then() invokes this function, the passed in function log (or console.log) does not have this bound to console. Note that if we invoke console.log(something), then when log is running, the this is bound to console. In the case of console.log, this is not important.

Note that we do .then(console.log), it is not any different from

const g = console.log;

fetch( ... )
  .then(res => res.json())
  .then(g)
Enter fullscreen mode Exit fullscreen mode

It is easier to see the g has the lost binding problem.

As a result, we need to be careful about

.then(a.b.c.fn)
Enter fullscreen mode Exit fullscreen mode

because fn has lost the binding to c. To make the binding stay we have to use

.then(a.b.c.fn.bind(a.b.c))
Enter fullscreen mode Exit fullscreen mode

And this brings another lesson in JavaScript: whenever we see something like foo.bar.fn or foo.fn, we need to be careful about the possibility of lost binding. And if we are debugging, being able to recognize this a.b.fn pattern would help you identify the possible bug due to the famous JavaScript lost binding problem.

Discussion (10)

Collapse
tbroyer profile image
Thomas Broyer

This is very error prone though!
As soon as the caller or callback can pass/take different arguments. And it will bite you as soon as you want to change the API (start passing more arguments to the callback, start taking more arguments in the callback)

Simple counter-example:

let ints = ["1","2","10","16","20"].map(parseInt);
Enter fullscreen mode Exit fullscreen mode
Collapse
lionelrowe profile image
lionel-rowe

It's only error prone if the outer function calls the callback with arity greater than 1, though. It's perfectly safe with .then chains, because .then only passes a single value, the value the promise resolves to.

Collapse
sargalias profile image
Spyros Argalias • Edited

Fair point. You have to be careful when the number of arguments passed can be a problem. However the majority of the time, I use functions that only accept a single argument or on something like .then or .catch which only passes a single argument, so I prefer OP's way for that.

I can see the problem with inconsistency though, which might lead to the type of errors you mentioned. As they say, "consistency over perfection", which would lead me to choose your method if we had to pick just one.

But, in my opinion, the best solution is for everyone in the codebase to be comfortable with functional programming (which is probably not realistic). If that was the case, then the parseInt solution might be something like this:

import {unary} from 'ramda';
const ints = ["1","2","10","16","20"].map(unary(parseInt));

// or

import {map} from 'ramda';
const ints = map(parseInt, ["1","2","10","16","20"]);
Enter fullscreen mode Exit fullscreen mode
Collapse
kennethlum profile image
Kenneth Lum Author • Edited

parseInt is the classic example... in this case the extra index passed into parseInt... for setData() it should be ok IMHO

Collapse
tbroyer profile image
Thomas Broyer

If you use setData with Array.prototype.map, it makes it harder to add an (optional) second argument to setData without breaking .map(setData); whereas .map(d => setData(d)) would work just as well, always passing a single argument to setData.

And using .then(parseInt) will make it harder for TC39 to possibly add a second argument to the ifFulfilled callback without breaking existing websites; whereas .then(d => parseInt(d)) would continue to work just as well, ignoring the second argument.

Using the function directly as the callback only works if it's been designed for that exact usage only.

Given all that, one should use d => setData(d) as rule of thumb, making it explicit which arguments are expected and used.

Thread Thread
kennethlum profile image
Kenneth Lum Author

I guess that can make sense. When I first looked at some code that was like

fetch(" ... ")
  .then(res => res.json())
  .then(setData)
  .catch(console.error);
Enter fullscreen mode Exit fullscreen mode

I just thought it was kind of cool. I guess maybe just as something we are aware of.

Collapse
sargalias profile image
Spyros Argalias

Yes, completely agree. After getting used to functional programming the redundancy of .then(d => something(d)); always sticks out to me as .then(something); is equivalent.

It's as though someone was writing:

function add5(a) {
  function nowActuallyRunTheThing(a) {
      return 5 + a
  }
  return nowActuallyRunTheThing(a);
}
Enter fullscreen mode Exit fullscreen mode

Instead of just:

function add5(a) {return a + 5};
Enter fullscreen mode Exit fullscreen mode

Sometimes I take it a step further and also shorten the res.json() line, although this one depends because sometimes it can be more confusing. For example:

import { invoker } from 'ramda';
fetch("...")
  .then(invoker(0, 'json')) // equivalent to (res => res.json())
Enter fullscreen mode Exit fullscreen mode
Collapse
ninofiliu profile image
Nino Filiu

no

Collapse
lionelrowe profile image
lionel-rowe

To be honest this works better as an argument against library authors making breaking changes, rather than an argument against library consumers using JavaScript to its full potential just in case library authors make breaking changes.

Collapse
lyrod profile image
Lyrod

TypeScript.