Design

React hook, can it be better?

I see react hook proposal today and while it is a solution to some problems, I am concerned with how they use universal functions (useState) rather than dependency inject them, which is clearer. In this article, I am exercising my ability at designing library by trying to find another approach to those problems.

First, we like to see their point about Complex components become hard to understand at introduction.

It pools subscribed events and execute it afterwards

From the example at the following gist, I feel that the implementation idea of useEffect is just subscribing events, pool them and execute by order when render has been done. I know the implementation above is not following the best practice, but the fact that it can fully works concern me.

However, personally I don’t know how that kind of design workaround can be exploited, either in good way or bad way. But one thing for sure, the same won’t happen when declared using class declaration, or the functionality being added using HOC, meaning no exploit may be present.

useState in custom hook blur the scope

useState is imported from React, which is kinda global-level module. I’m using the following gist for this example.

I’d like to know, at which scope the useState here resides?  At first, based on the design, I think it’s singleton, and globally managed. But seeing the implementation, it is scoped per useState call.

If you run the code, it’s logged 20 times, which are called per every useState call, multiplied per each Elm rendered, doubled because one on didMount and another on didUpdate (not quite correct, but it’s similar enough).

I know the scope is documented. But as I have stated before, the fact that class declaration does not have this scope issue makes this design somehow confusing.

Can it be better?

Better is usually relative to something. I prefer it to be somehow have more explicit declaration and better at clearer flow. But it’s verbose and increasing code size, which is worse in that aspect.

Reusable Logic

First, the reusable logic for component that envisioned by react team is more or less consist of those aspects:

  • can maintain it’s own state
  • can have parameters, which is independent with component’s props
  • can have state changed and component attached will able to watch it
  • state can be changed asynchronously
  • can have some logic at side effect phase, ex: at didMount

Functional Component

Then the functional component somehow has those aspects:

  • can maintain it’s own state
  • can use / attach many Reuseable Logic and watch it’s state
  • can have some logic at side effect phase, ex: at didMount

The design

This gist is the example of how react hooks declared that I envisioned. For simplicity, I use the obsolete createClassmethod to indicate component composition.

At useFriendStatus:

  • it accept a parameter of self-managing state
  • then it return function that accept parameters specified by developers
  • then the body is where the logic reside
  • finally, it’ll return an object, which contains state that can be listened, and side effect (if any)
  • the effect can return another function, in which will be executed on willUnmount

Then at FriendStatus or component, it has 2 parameters. The 2nd one being the specification of component:

  • it will specify initialState to be passed to component
  • it also specify which hooks to attach, by injecting props for initial state, and createState that can be used to create self-managing state for reusable logics
  • the hooks specification will return array of reusable logic, which already been injected with each of their own self-managing state

Then the 1st being the render part:

  • as usual, the normal props injected at 1st param
  • 2nd param being the self-managing state with initialState value from specification
  • 3rd param being the list of hook states that can be watched. It’s positionally same with hooks definition position from specification

What is the difference?

For me, my version explicitly show where is the state comes from. It’s living scope is also clear because it’s defined in hooks specification. And it’s also not using any universal-level functions except for builder-pattern-like createClass.

The hooks specification being executed once and defined in specification makes it more explicit declaration. The reusable logic being pure function also makes it easier to test, mock or reusable with other libraries. Of course some side effect and conditional hacks can also be applied, but it’s another topic that can be discussed later.

However it’s very clear that it’s worse in terms of code size, since it’s add more details to code. And it’s also reduce the number of manageable state to one per each reusable logic.

Conclusion

Tweaking a RFC concept and trying to find the flaws and better approach is a fun experience. It can also serve as exercise for me at designing API for library.

Mutability: Array

Mutability in programming is the ability of an object to have it’s state / value changed. Immutable object, on the other hand, cannot have it’s state changed. In php, javascript, C# and Java, most of variables / objects that is commonly used are mutable. Array is one of them.

Array mutability

Let’s see the following snippet of code:

let exec1 = function() {
    console.log("START: EXEC 1");
    let a = [3, 5, 7];
    let wrongOperation = function(arr){
        arr.push(8);
        arr[1] = 4;

        return arr;
    };

    let b = wrongOperation(a);
    console.log("b:");
    b[2] = 6; // mutation
    console.log(b); // results [ 3, 4, 6, 8]

    console.log("a:");
    console.log(a); // results [ 3, 4, 6, 8]
    console.log("DONE: EXEC 1");
    console.log();
};

We can see that any modification to b is changing the value of a, which sometimes is expected, and the other times are becoming a bug. If the result b are returned and modified by another caller. Sometimes in the future, you may wonder why the content of a changed. It will be hard to track where the changes happen to a.

A better design

A better design is indeed, to prevent any changes made to b to be reflected back to it’s original owner, a. This can be achieved by replicating the array argument using concat in javascript, or array_merge in ​php to an empty array. See the example in following snippet:


let exec2 = function() {
    console.log("START: EXEC 2");
    let a = [3, 5, 7];
    let correctOperation = function(arr){
        let result = [].concat(arr);
        result.push(8);
        result[1] = 4;

        return result;
    };

    let b = correctOperation(a);
    console.log("b:");
    console.log(b); // results [ 3, 4, 7, 8]

    console.log("a:");
    console.log(a); // results [ 3, 5, 7]
    console.log("DONE: EXEC 2");
    console.log();
};

Above example shows how the operation copy the argument array first before doing any operation using concat with another empty array. It cause any further modification after that function to not reflect back to original variable a.

Another example can be like this:


let exec3 = function() {
    console.log("START: EXEC 3");
    let a = [3, 5, 7];
    let anotherCorrectOperation = function(arr){
        let newData = [];
        for(let i = 2; i < 4; i++){
            newData.push(i);
        }
        return arr.concat(newData);
    };

    let b = anotherCorrectOperation(a);
    console.log("b:");
    b[1] = 4; // test mutability
    console.log(b); // results [ 3, 4, 7, 2, 3]

    console.log("a:");
    console.log(a); // results [ 3, 5, 7]
    console.log("DONE: EXEC 3");
    console.log();
};

The above example do operations first, then returning the operation result together with the existing array. This is the preferred approach to the other for-push​ directly to argument array.

It’s still just the array that be copied

However, both preferred example above only copied and un-ref the array and array only. The content is still the same, and can be modified. For example, if the array contains javascript objects, any modification to the array member will be reflected back to original variable a:

let exec4 = function() {
    console.log("START: EXEC 4");
    let a = [{v: 1}, {v: 3}];
    let anotherCorrectOperation = function(arr){
        let newData = [];
        for(let i = 2; i < 4; i++){
            newData.push({v: i});
        }
        return arr.concat(newData);
    };

    let b = anotherCorrectOperation(a);
    console.log("b:");
    b[1].v = 4; // test mutability
    console.log(b); // results [ { v: 1 }, { v: 4 }, { v: 2 }, { v: 3 } ]

    console.log("a:");
    console.log(a); // results [ { v: 1 }, { v: 4 } ]
    console.log("DONE: EXEC 4");
    console.log();
};

So you still need to be careful at doing variable modification inside functions. However at least you need not to worry anymore when doing modification to the array.

Conclusion

Design your operation to be immutable and returning copy by default, unless the other behavior is somewhat desired. This can help to make code easier to track, modular and prevent unnecessary bug in the future. All code that is shown in this article can be retrieved at my github repository.