Why React Hooks appear

import { Request } from 'express';
import { Controller, Get, Post } from './core';

@Controller('/test')
export class TestController {
  @Get()
  async getAll() {
    return 'TEST !!!';
  }

  @Get('/a')
  async getA() {
    return 'WTF A';
  }

  @Post('/b')
  async setB(req: Request) {
    return `Request Body is ${JSON.stringify(req.body)}`;
  }
}

Reflect & Decorator

import { Component, OnInit, Input } from '@angular/core';

@Component({
  selector: 'bar',
  templateUrl: './bar.component.html',
  styleUrls: ['./bar.component.css']
})
export class BarComponent implements OnInit {
  @Input() bar: string

  constructor() { }

  ngOnInit() {
  }
}

Angular

import { Component, State } from 'react';

@Component()
export class CounterComponent extends Component {
  @State() count: number = 1;
  
  addOne = () => {
  	this.count += 1;
  }
  
  render() {
    return (
      <div>
        count: {this.count}
        <button onClick={this.addOne}>add one</button>
      </div>
    );
  }
}

At the begining

React want class component to be

<template>
  <div>
    <button v-on:click="decrement">-</button>
    {{ count }}
    <button v-on:click="increment">+</button>
  </div>
</template>

<script>
import 'reflect-metadata';
import { Vue, Component, Prop } from 'vue-property-decorator';

@Component
export default class Counter extends Vue {
  @Prop() initialCount: number;

  count = this.initialCount;

  increment() {
    this.count++;
  }

  decrement() {
    this.count--;
  }
}
</script>

Vue 2 decorator support

export interface CounterProps {
  initialCount: number;
}

@Hookable()
class Counter extends HookableComponent<CounterProps> {
  @State() count = this.props.initialCount;

  addOne = () => {
    this.count += 1;
  }

  render() {
    return (
      <div>
        <p>Count: {this.count}</p>
        <p>Initial count: {this.props.initialCount}</p>
        <button onClick={this.addOne}>Increase</button>
      </div>
    );
  }
}

Example - for fun

import "reflect-metadata";

const HOOK_METADATA = "__hooks__";

export abstract class HookableComponent<P = {}> {
  constructor(public props: P) {}
  abstract render(): JSX.Element;
}

type HookableHook = (instance: HookableComponent) => void;

const defineHookMetadata = (target: Object, hook: HookableHook) => {
  const hooks = Reflect.getMetadata(HOOK_METADATA, target) || [];
  hooks.push(hook);
  Reflect.defineMetadata(HOOK_METADATA, hooks, target);
};

export const State = (): PropertyDecorator => (target, propertyKey) => {
  const hook: HookableHook = instance => {
    const initialValue = instance[propertyKey as keyof typeof instance];
    const [value, setvalue] = useState(initialValue);

    Reflect.defineProperty(instance, propertyKey, {
      get: () => value,
      set: setvalue
    });
  };

  defineHookMetadata(target, hook);
};
export const Hookable = (): ClassDecorator => target => {
  function HookableComponent(props: any) {
    const ref = useRef<HookableComponent>();

    if (!ref.current) {
      ref.current = Reflect.construct(target, [props]);
    }

    const instance = ref.current!;
    instance.props = props;
    const hooks: HookableHook[] = Reflect.getMetadata(HOOK_METADATA, instance);
    hooks.forEach(hook => hook(instance));
    return instance.render();
  }

  HookableComponent.name = target.name;

  return HookableComponent as any;
};

But
React & Vue(v3)
Decide to use
composition function api

Why they deprecate using class based component api ?

Class in JavaScript

 You have to understand how this works in JavaScript, which is very different from how it works in most languages

Without unstable syntax proposals, the code is very verbose.

class SomeComponent extends React.Component {
  constructor() {
    this.onClick = this.onClick.bind(this);
  }
  
  onClick() {
    // ...
  }
}

class SomeComponent extends React.Component {
  onClick= () => {
    // ...
  }
}

Decorator proposal

Still on stage 2 (Draft)

TypeScript Support

interface Theme {
  // ...
}

const ThemeContext = createContext<Theme>(defaultTheme);

@Hookable()
class SomeComponent extends HookableComponent<CounterProps> {
  @Context(ThemeContext) theme: Theme;
  
  // ...
}

const SomeComponent: FC = () => {
  const theme = useContext(ThemeContext);
};

Type Inference

Reuse logic

@Hookable()
class CounterMixin extends HookableMixin<CounterProps> {
  @State() count = this.props.initialCount;

  addOne() {
    this.count++;
  }
}


@Hookable()
class TwoCounter extends Mixins(CounterMixin, CounterMixin) {
  // ...
}

Naming Conflict

function createCouterMixin(config) {
  @Hookable()
  class CounterMixin extends HookableMixin<CounterProps> {
    @State() count = this.props.initialCount;

    addOne() {
      this.count++;
    }
  }

  return CounterMixin;
}


@Hookable()
class TwoCounter extends Mixins(CounterMixin(/* ... */), CounterMixin(/* ... */) {
  // ...
}

Naming Conflict

function useCounter(initialCount: number) {
  return useState(initialCount);
}

const TwoCounter: FC = props => {
  const [count1, setCount1] = useState(props.initialCount1);
  const [count2, setCount2] = useState(props.initialCount2);
};

Composition

Appendix

So why Angular still use decorator and class ?

AOT

ahead of time compilation

Transpile + PreCompile

(function () {
  function fibonacci(x) {
    return x <= 1 ? x : fibonacci(x - 1) + fibonacci(x - 2);
  }
  window.x = fibonacci(15);
})();
window.x = 610;

Pre Compile

(function () {
  var self = this;
  ['A', 'B', 42].forEach(function(x) {
    var name = '_' + x.toString()[0].toLowerCase();
    var y = parseInt(x);
    self[name] = y ? y : x;
  });
})();
_a = "A";
_b = "B";
_4 = 42;

Common Transpilers

In Ecosystem of ECMAScript

const [a,,b] = [1, 2, 3];
"use strict";

var _ref = [1, 2, 3],
    a = _ref[0],
    b = _ref[2];
const args = [1, 2, 3];

const foo = (...nums) => nums.reduce((acc, num) => acc + num, 0);

foo(...x);
"use strict";

var args = [1, 2, 3];

var foo = function foo() {
  for (var _len = arguments.length, nums = new Array(_len), _key = 0; _key < _len; _key++) {
    nums[_key] = arguments[_key];
  }

  return nums.reduce(function (acc, num) {
    return acc + num;
  }, 0);
};

foo.apply(void 0, x);
<script>
  let count = 0;

  function handleClick() {
    count += 1;
  }
</script>

<button on:click={handleClick}>
  Clicked {count} {count === 1 ? 'time' : 'times'}
</button>

Svelte

/* App.svelte generated by Svelte v3.19.2 */
import {
	SvelteComponent,
	append,
	detach,
	element,
	init,
	insert,
	listen,
	noop,
	safe_not_equal,
	set_data,
	space,
	text
} from "svelte/internal";

function create_fragment(ctx) {
	let button;
	let t0;
	let t1;
	let t2;
	let t3_value = (/*count*/ ctx[0] === 1 ? "time" : "times") + "";
	let t3;
	let dispose;

	return {
		c() {
			button = element("button");
			t0 = text("Clicked ");
			t1 = text(/*count*/ ctx[0]);
			t2 = space();
			t3 = text(t3_value);
		},
		m(target, anchor) {
			insert(target, button, anchor);
			append(button, t0);
			append(button, t1);
			append(button, t2);
			append(button, t3);
			dispose = listen(button, "click", /*handleClick*/ ctx[1]);
		},
		p(ctx, [dirty]) {
			if (dirty & /*count*/ 1) set_data(t1, /*count*/ ctx[0]);
			if (dirty & /*count*/ 1 && t3_value !== (t3_value = (/*count*/ ctx[0] === 1 ? "time" : "times") + "")) set_data(t3, t3_value);
		},
		i: noop,
		o: noop,
		d(detaching) {
			if (detaching) detach(button);
			dispose();
		}
	};
}

function instance($$self, $$props, $$invalidate) {
	let count = 0;

	function handleClick() {
		$$invalidate(0, count += 1);
	}

	return [count, handleClick];
}

class App extends SvelteComponent {
	constructor(options) {
		super();
		init(this, options, instance, create_fragment, safe_not_equal, {});
	}
}

export default App;
import { Component } from '@angular/core';

@Component({
  selector: 'my-component',
  template: '{{person.addresss.street}}'
})
class MyComponent {
  person?: Person;
}

??

React

However, we found that class components can encourage unintentional patterns that make these optimizations fall back to a slower path. Classes present issues for today’s tools, too.

import { Component } from '@angular/core';

@Component({
  selector: 'my-component',
  template: '{{person.addresss.street}}'
})
class MyComponent {
  person?: Person;
}

my.component.ts.MyComponent.html(1,1): : Property 'addresss' does not exist on type 'Person'. Did you mean 'address'?

Template Type Check

@Injectable()
class CounterService {
  readonly count$ = new BehaviorSubject(0);
  
  increment(num: number) {
    this.count.next(this.count.value + number);
  }
}

@Component({
  selector: 'counter',
  template: `
    <button (click)="counter.increment(1)">add one</button>
    <span>{{ counter.count$ | async }}</span>
  `
})
class TwoCounterComponent {
  count: number;

  constructor(
    public readonly counter: CounterSerivce
  ) {}
}

Services w/ DI container

Roast

import React from 'react';

const ThemeContext = createContext();
const AuthContext = createContext();
const MyContext = createContext();


<ThemeContext.Provider>
  <AuthContext.Provider>
    <MyContext.Provider>
      // ...

Where is useProvide ?

Q & A

Why React Hooks appear

By jjaayy

Why React Hooks appear

  • 427