0

Is it possible to chain a React hook? If so, how?

A typical application of a hook would look like this:

const [inv, updateInventory] = useState([])
a = ["cheese", "bread", "apples"]
b = a.filter(isDairy)
updateInventory(b)

We can also do this, but it's not chained:

const [inv, updateInventory] = useState([])
a =  ["cheese", "bread", "apples"]
updateInventory(a.filter(isDairy))

What I want is a chained hook in a functional style:

const [inv, updateInventory] = useState([])
a =  ["cheese", "bread", "apples"]
a.filter(isDairy).updateInventory()

Can a hook can be modified to take state from this?

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
kmiklas
  • 13,085
  • 22
  • 67
  • 103
  • 1
    You could import stage 1 and use the pipeline operator: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Pipeline_operator but I don't really see the value in doing this. – Hunter McMillen Jul 20 '20 at 14:53
  • 1
    You can add arbitrary methods to arrays by patching the prototype, but I wouldn't recommend it - how many hooks do you have with array values? Do you want them *all* to be methods on arrays? – jonrsharpe Jul 20 '20 at 14:54
  • 1
    Note if you do decide to patch arrays (remember `updateInventory` would be on *every array*, though!) this becomes https://stackoverflow.com/q/948358/3001761 – jonrsharpe Jul 20 '20 at 15:14

2 Answers2

1

Proper usage would be:

updateInventory([...a, "cheddar"].quicksort().filter("cheese"))

But if you really want that chaining, look into how to edit the array prototype.

This is really not recommended, as that method will then be available on all arrays.

Luke Storry
  • 6,032
  • 1
  • 9
  • 22
  • I'm thinking ``[...a, "cheddar"].quicksort().filter("cheese").updateInventory()``. Essentially what I'm asking is can the hook be modified to use ``this`` and support chaining? Assuming the ``.filter("cheese")`` method returns ``this``, can updateInventory() be modified to take state from ``this``? – kmiklas Jul 20 '20 at 18:08
  • 1
    the `.filter` method returns an Array, so its `this` would be an array, so you'd have to extend the prototype of `array` to do the chaining as you wish – Luke Storry Jul 20 '20 at 18:13
  • 1
    @kmiklas as I pointed out a few hours ago, you'd have to patch every array in your application to *"to use `this` and support chaining"*. This isn't really anything to do with React. – jonrsharpe Jul 20 '20 at 18:55
  • This business of extending the array prototype is too narrow an answer; it doesn't speak to the question. I can easily set ``this.arr`` and ``return this``. Is there a way to chain into the hook? – kmiklas Jul 21 '20 at 00:51
0

I think the underlying problem is you're not clear on what's actually happening with method chaining and possibly with hooks. The specific question:

Can a hook can be modified to take state from this?

doesn't really make sense. So let's break down why then come back at the end to how you could approach this.


For method chaining, let's try a simple example using two methods, .filter and .map, that have two important properties:

  1. They actually return arrays (unlike .push, which returns the new length of the array); and
  2. They actually exist on arrays (unlike .quicksort, which exists on neither an array nor the integer you were calling it on).
function isDairy(item) {
  return ["cheese", "milk"].includes(item);
}

function getPrice(item) {
  return { bread: 0.58, cheese: 0.80, apples: 0.47, milk: 1.01 }[item];
}

const inStock = ["bread", "cheese", "apples"];

inStock
  .filter(isDairy)
  .map((item) => ({ item, price: getPrice(item) }));
// => [{ item: "cheese", price: 0.8 }]

There's nothing particularly special happening here, each method you're calling returns a new array on which you can also call any method an array has. You could assign the intermediate steps and get the same result:

const filteredStock = stock.filter(isDairy);
// => ["cheese"]
const pricedFilteredStock = filteredStock.map((item) => ({ item, price: getPrice(item) }));
// => [{ item: "cheese", price: 0.8 }]

It is not the case that:

  • these are standalone functions (like in e.g. Python where you map(callable, iterable)); or
  • that the item.name syntax is doing anything beyond just accessing a property named name on the item.

If I tried to use the filter method as a standalone function:

filter(isDairy, inStock);

that would be a ReferenceError, or if I defined another function and tried to access it as if it was a prop on an array:

function allUppercase() { 
  return this.map((item) => item.toUpperCase());
}

inStock.allUppercase();

it would be a TypeError (because isStock.allUppercase is undefined and undefined isn't callable).

Note you could do allUppercase.bind(inStock)() (or the neater allUppercase.call(inStock)), though; JavaScript does have a means of setting this for a function.


When you use the useState hook, you're calling a function that returns an array containing two objects, and destructuring that array to two local variables:

const [thing, setThing] = useState(initialValue);

is equivalent to:

const result = useState(initialValue);
const thing = result[0];
const setThing = result[1];

The thing, setThing naming is just a convention; really, we're accessing those two objects (current value and setter function) by position. They don't have names of their own, you can do const [foo, bar] = useState("baz") (but... don't).

As the setter is a function you might be wondering whether you can use setThing.bind here, but if setThing is written to use this (I didn't look into the implementation, as it's not directly relevant), it's not going to be happy if you change what this is!


So this comes together when you try to do:

const [basket, setBasket] = useState([]);
            // ^^^^^^^^^

inStock.filter(...).map(...).setBasket();
                          // ^^^^^^^^^

As with the example above, this is a TypeError because setBasket doesn't exist on the array returned by .map. The fact that the same "word" setBasket appears twice is totally irrelevant as far as JavaScript is concerned; one is a local variable and the other is a prop on an array, there's no connection between them.

.map(...) returns a new array, one that we didn't already have a reference to, so the only way to make this work is to ensure all arrays have a setBasket method, which means patching the prototype (as covered in adding custom functions into Array.prototype):

Object.defineProperty(Array.prototype, "setBasket", {
  value () {
    setBasket(this);
  },
});

One problem here is that the function setBasket is accessed via a closure, so it needs to happen inside the component where the hook is defined, so it's going to get defined every time the component is rendered (or you're going to useEffect), which is a problem because you can't redefine that method as written...

But let's ignore that because the bigger problem is that every array in your app now has that method, even in contexts where it's not relevant. If you have multiple state hooks, as seems likely in any non-trivial app, your arrays are gaining lots of methods globally that are only for use in small local scopes.


A more feasible approach is to add a generic method that can be used to apply any hook (in fact any function) to an array:

Object.defineProperty(Array.prototype, "andCall", {
  value (func) {
    return func(this);
  },
});

This can be added once, globally, and used to apply whatever hook is relevant:

inStock.filter(...).map(...).andCall(setBasket);

Note that if you're using TypeScript, you'd also have to add the definition to the global array type, e.g.:

declare global {
    interface Array<T> {
        andCall<S>(func: (arr: Array<T>) => S): S;
    }
}
jonrsharpe
  • 115,751
  • 26
  • 228
  • 437