Preact Hooks Internal

เดือนที่แล้วนั่งอ่าน Inferno ว่าจะทำ Hook มั้ย

คืออยากหา framework ที่คิดเหมือนเราว่า Hook มันคือ Magic และมัน implement ได้โดยไม่ต้องใช้ Magic นั่นแหละ

ก็ยังไม่รู้ว่า Inferno คิดยังไง แต่ก็มีคนลองๆ ทำอยู่ ซึ่งเค้าก็ไปลอก Preact มาอีกที ไม่รู้ว่าจะเหมือนกับ React มั้ย แต่คิดว่าอ่านแล้วแล้วเข้าใจง่ายดีเลยเอามา blog ไว้หน่อย

useState

ซอร์สของ Preact Hook อยู่ที่ hooks/src/index.js เราจะไล่จากด้านนอกที่เราคุ้นเคยไปก่อนน่าจะง่าย นั่นคือ useState ซึ่งพื้นๆ มาก…

export function useState(initialState) {
	currentHook = 1;
	return useReducer(invokeOrReturn, initialState);
}

ปรากฏว่า useState คือ wrapper บน useReducer ซึ่งก็ทำให้หายสงสัยว่าทำไม React ต้องมี useReducer API ทั้งที่ๆ คนปกติก็จะไปใช้ Redux อยู่แล้ว

ส่วน invokeOrReturn คิดว่าถ้าจำได้ว่า reducer เขียนยังไง แล้ว setState function มันรับอะไรน่าจะพอเดาได้ว่ามัน implement ยังไง

function invokeOrReturn(oldState, f) {
	return typeof f == 'function' ? f(oldState) : f;
}

ง่ายๆ ก็คือมันคืนค่าที่เราเรียกนี่แหละ แต่ถ้า setState เรียกด้วย function มันจะได้ state เก่าเป็น argument แล้วเราคืนค่าใหม่มาแทน

useReducer

export function useReducer(reducer, initialState, init) {
	const hookState = getHookState(currentIndex++, 2);
	hookState._reducer = reducer;
	if (!hookState._component) {
		hookState._component = currentComponent;

		hookState._value = [
			!init ? invokeOrReturn(undefined, initialState) : init(initialState),

			action => {
				const nextValue = hookState._reducer(hookState._value[0], action);
				if (hookState._value[0] !== nextValue) {
					hookState._value = [nextValue, hookState._value[1]];
					hookState._component.setState({});
				}
			}
		];
	}

	return hookState._value;
}

ขอข้าม getHookState ไปก่อนที่จะอธิบายต่อไป

useReducer มี state คือ

  • Reducer function ที่เราส่งเข้าไป
  • Return value ของมันที่ cache ไว้ ก็คือ [state, dispatch] อยู่ใน __value
  • Component instance ซึ่งมันเก็บไว้เรียก setState({}) เพื่อ invalidate view

จะเห็นว่ามันใช้ global state อยู่ 2 อันคือ currentComponent และ currentIndex แล้วใครเซตตัวแปรพวกนี้?

Actual hooks

Preact internal มีส่วนที่เปิดให้ implement functionality เพิ่มเติมได้ซึ่งมันเรียกว่า hook ซึ่งเป็น hook ในความหมายดั้งเดิมคือเราเอา function ไปผูกไว้กับอีกส่วนของ library หรือถ้าใครเขียน Vue มันคือ Hook ในความหมายของ Lifecycle hook นั่นแหละ

Hook ที่เราสนใจคงเป็น _render hook ซึ่งใน docs มันเขียนว่า “Attach a hook that is invoked before a vnode has rendered”

_render hook ของ hook เขียนว่า

options._render = vnode => {
	if (oldBeforeRender) oldBeforeRender(vnode);

	currentComponent = vnode._component;
	currentIndex = 0;

	const hooks = currentComponent.__hooks;
	if (hooks) {
		hooks._pendingEffects.forEach(invokeCleanup);
		hooks._pendingEffects.forEach(invokeEffect);
		hooks._pendingEffects = [];
	}
};

VNode

ถ้าใครเขียน Vue จะเห็นว่า Vue expose VNode API ออกมาให้ด้วย แต่ใน React API ไม่มีให้ใช้เพราะถือเป็น internals

VNode คือ Virtual DOM node นั่นแหละ (Component เราไม่ได้เป็น VNode ตรงๆ แต่จะถูกครอบอีกทีนึง อย่างที่เห็นว่ามันต้อง read vnode._component ออกมาในโค้ดด้านบน)

ฉะนั้นสิ่งที่มันทำก็คือ ทุกครั้งที่เราอยู่บน node ใหม่ มันจะเซฟ currentComponent เก็บไว้ แล้วรีเซต currentIndex เป็น 0

นี่คือ Magic ของ Hook คือแทนที่มันจะเก็บ local state แบบ this.xxx เค้ากลับเลือกที่จะยกออกไปเป็น global ภายในตัว framework

ที่ทำได้เพราะ JavaScript code เป็น single thread (ไม่ต้องกังวลว่ารันโค้ด concurrent แล้วจะมีคนเขียนทับ global) และ Preact rendering ก็เป็น synchronous (ไม่มีแบบว่า render อยู่แล้วสลับไปสลับมา)

ที่น่าสนใจต่อคือ React fiber กำลังจะทำ asynchronous rendering แล้วมัน implement ตรงนี้ยังไงกันนะ?

getHookState

เมื่อกี้เราติด getHookState ไว้

const hookState = getHookState(currentIndex++, 2);

โค้ดของ getHookState คือ

function getHookState(index, type) {
	if (options._hook) {
		options._hook(currentComponent, index, currentHook || type);
	}
	currentHook = 0;

	const hooks =
		currentComponent.__hooks ||
		(currentComponent.__hooks = {
			_list: [],
			_pendingEffects: []
		});

	if (index >= hooks._list.length) {
		hooks._list.push({});
	}
	return hooks._list[index];
}

ส่วนแรกคือ Hook เองเปิดโอกาสให้ใส่ functionality hook เข้าไปได้อีก มันเลยต้องมีตัวแปร currentHook เพื่อบอกให้คนที่เข้ามาต่อรู้ว่ากำลังจะ execute hook ประเภทไหนอยู่ สำหรับค่าต่างๆ มีประกาศไว้ใน type definition

ถัดมามันก็จะอ่าน __hooks บน component ปัจจุบัน หรือสร้างใหม่ ด้านในจะมี array อยู่ซึ่งมันจะอ่านตัวที่ currentIndex ที่ส่งเข้ามา ซึ่งในโค้ด useReducer จะเขียนว่า currentIndex++ ก็คืออ่านค่าปัจจุบันแล้วเลื่อน index ไปลำดับถัดไป

นั่นคืออีก Magic ของ hook ที่ทำให้ function ธรรมดาเรียกสองครั้งแล้วได้ค่าไม่เหมือนเดิม เพราะมันแอบมี global state อยู่นั่นเอง แต่ global state นั้นถูก reset ระหว่าง component ทำให้รู้สึกเหมือนว่ามันเป็น local state

ถ้าถามผมผมก็ยังคิดว่า local state มันควรจะเป็น attribute บน this แล้วใช้ mixin เพื่อ compose behavior ซึ่ง React เคยมีแต่ถอดไปนานแล้ว เพราะมักจะเจอปัญหาตั้งชื่อชนกัน หรือใช้ 2 อันไม่ได้ ใน Hook ก็เลยไม่ให้ตั้งชื่อแล้วทำเป็น index เลื่อนไปเรื่อยๆ แทน

และนี่คือที่มาของกฎของ hook ว่าทำไมห้ามใช้ hook ใน if เพราะลำดับจะเลื่อนไม่ตรงกันแล้ว hook state จะผสมกันมั่ว

ที่น่าสนใจคือ ยังไม่มีตรงไหนบอกว่าทำไม Hook ใช้กับ class component ไม่ได้ ซึ่งถ้าไปอ่าน issue ต้นเรื่อง ทีม Preact บอกว่า

You may be wondering why we mix those and the reason is simply to save some bytes. And yes, this allows hooks to be used in class components! We don’t really advertise that though 👍

บน Preact ใช้ Hook บน class component ได้! (แต่ไม่ใช่ public API)

สรุป

สิ่งที่ Preact Hook ทำคือ

  1. เมื่อกำลังจะ render vdom node ใหม่ ให้เซต currentComponent และเซต currentIndex=0
  2. เมื่อเราเรียก hook มันจะไปอ่าน state บางอย่างบน component ปัจจุบัน
  3. อ่านแล้วมันจะเลื่อน pointer ไปยังช่องถัดไปเพื่อให้ hook ตัวถัดไปได้ state ไม่ตรงกัน

Final thoughts

หลังอ่านจบก็เริ่มสงสัยว่าคนเขียน React สักกี่คนนะที่เชียร์ Hook แล้วเข้าใจจริงๆ ว่า Hook มัน implement อย่างไร (ให้ project เปล่าๆ ห้ามโหลดของจาก npm เขียน Hook ยังไง?)

ก็เกือบไปลองออกเป็นข้อสอบ interview ดู แต่คิดดูอีกทีหนึ่งแล้ว คนที่เขียน Hook ได้คือคนที่แม่น JavaScript (ซึ่งเรา require) แต่นั่นยังไม่ใช่ทั้งหมดที่จะเป็น frontend dev ที่ดีได้ (ยังมีเรื่องอื่นๆ อีกเยอะแยะ เช่น CSS, box layout, sematic markup, web optimization, SEO) มันไม่น่าจะเป็นข้อสอบที่ดีเท่าไร

ในขณะเดียวกัน เอาไปเทส backend dev ก็ไม่ได้เหมือนกันเพราะ React เป็นเรื่องของ Frontend

แล้วใน software team จะให้ role ไหนเป็นคนสร้าง React?

มันเป็นโจทย์ที่ยากมากและ require ความรู้ลึกทั้งฝั่ง backend (โดยเฉพาะ VDOM implementation กับ Suspense) และ frontend เลย หรือนั่นคือนิยามจริงๆ ของ Full stack developer กันนะ

ลอง Mithril.js

หลังจากเขียน Vue ไปเกือบเสร็จ มารู้ว่า Vue จะทำ hook โดยเฉพาะที่ Vue 3 อยากจะเปลี่ยน API เป็น function หมดแล้วโดนด่าต้องถอยสุดซอย การันตีว่าจะไม่เปลี่ยนอีกใน Vue 3.x (แล้ว 4 ล่ะ?) ก็เลยว่าจะ rewrite

สรุปว่า Project นี้เปลี่ยน framework ไปแล้ว 3 ตัว คือ Svelte (รีวิวไว้ใน Facebook ตั้งแต่ Svelte 3 ออก), Vue และ Mithril

Mithril นี่ claim มันคือมันบอกว่า API design มันคิดมาถูกตั้งนานแล้ว React อ่ะ design มั่วถึงต้องเปลี่ยนไปเปลี่ยนมา

แถม bundle size เล็กมาก 10kB เท่านั้น แล้วไม่ใช่แค่ view library อย่างเดียวแต่ใน 10kB นี้รวม router, promise polyfill และ XHR library แล้วด้วย!! แล้วเค้าก็ขายต่อว่า เพราะโค้ดมันเล็ก มันเลย review ได้ง่ายว่าปัญหาอยู่ตรงไหน performance ก็จะดีตาม

ส่วนว่าทำไม Mithril มีมาตั้งแต่ 6 ปีแล้วแต่ไม่ดัง บางคนบอกว่า “อ๋อ คนเขียน Mithril เค้าไม่ว่างเชียร์ พอดีกำลังเพลิดเพลินกับการเขียนโค้ดอยู่”

เอาสิ คนเขียน Vue ยังไม่ขิงขนาดนี้อ่ะ

Reactivity

View library ยุคใหม่ๆ จะเป็น Declarative หมด ก็คือเราบอกว่า interface หน้าตาแบบนี้ แล้ว library จะไปหาทาง update UI บนหน้าจอให้ตรงกับสิ่งที่เราบอกเอง

แต่สิ่งที่แสดงบนหน้าจอมักจะ depend กับ state เมื่อ state เปลี่ยนต้อง rerender interface ใหม่ ซึ่งวิธีที่แต่ละ Library ทำก็จะแตกต่างกันออกไป…

  • AngularJS 1 ใช้ dirty checking โดยมันจะแอบ clone state เก่าไว้ พอเราแก้มันจะ diff state เก่ากับใหม่เพื่อดูว่า state ตรงไหนเปลี่ยน แล้ว view ตรงไหน depend กับ state นี้
  • MobX 4 กับ VueJS 2 ใช้วิธี rewrite ทุก field บน state object ให้มี getter setter เพื่อที่จะได้ track การใช้งานได้ว่ามีการ set field นี้ แล้วใคร get มันบ้าง
  • MobX 5 กับ VueJS 3 ใช้ Proxy แทนการ rewrite field
  • Svelte ใช้ compiler rewrite field assignment ให้ไป mark ว่า field นี้มีการเปลี่ยนแปลง
  • React บังคับให้ state เป็น Immutable เพื่อให้ set state ได้ทางเดียว เมื่อ set แล้วก็จะ rerender component ที่เปลี่ยน state เพราะไม่รู้ว่าตรงไหนใช้อะไรบ้าง
  • Redux ใช้ Immutable state เช่นกัน เมื่อมีการ dispatch action แล้ว listener จะยิงให้ view library refresh

คำตอบของ Mithril คือ Mithril ไม่มี reactivity

ถ้าใครเขียน AngularJS 1 หรือ MobX จะรู้ว่าการเปลี่ยนค่าตัวแปรจะต้องรันในฟังค์ชั่นที่อนุญาตเท่านั้น ไม่อย่างนั้นมันอาจจะไม่อัพเดต ใน angular เรียกว่า $apply หรือ MobX เรียกว่า action เมื่อออกจาก action แล้ว library จึงจะเช็คว่าต้องแจ้งใครให้อัพเดตตามบ้าง

ใน Mithril โค้ดก็จะคล้ายๆ กัน แต่ไม่ต้องครอบฟังค์ชั่น เพียงแค่เรียก m.redraw() หลังจากแก้ state เสร็จแล้วเท่านั้นเอง แล้วมันจะ rerender ทั้งหน้าเว็บหลัง requestAnimationFrame

แล้วจุดที่มักจะมี state change ใน Angular เองก็จะครอบ $apply ให้ ใน Mithril ก็จะทำให้เช่นกัน ได้แก่ event handler และ หลัง AJAX call (ถ้าใช้ m.request)

ผมไม่แน่ใจว่าผมรู้สึกยังไงกับฟีเจอร์นี้ การทำ fine grained update แบบ Angular อาจจะฟังดูเร็วกว่าแต่มันก็มี CPU overhead และเพิ่ม code size ที่ต้อง track change ในขณะที่การรีเฟรชทั้งหมดก็เปลือง CPU

Global

State ของ Mithril เป็น Global เกือบทั้งหมดจนผมกังวลใจ

เช่น ฟังค์ชั่น m.redraw() ที่เล่าไปด้านบน นั่นคือ Signature เต็มๆ ของมันแล้วเพราะ Mithril เก็บ state เป็น global ไม่ต้องบอกว่าจะ redraw อะไร

หรือ Mithril router เองก็ใช้ m.router.get() อ่านค่า Route ล่าสุดโดยไม่ต้องพยายามหา router instance

เรื่องนี้คิดว่าเป็นเรื่องหนึ่งที่ยากจะกลืน พอๆ กับที่ React เอา HTML กับ JavaScript ผสมกัน (คนเขียนเว็บยุคเก่าจะบอกว่ามันผิด Separation of Concern) แต่คิดว่าที่ออกแบบมาแบบนี้เพราะ Mithril ไม่ได้คิดเรื่อง server side rendering เท่าไรนัก โค้ดที่ใช้ทำ SSR เองก็อยู่นอก codebase หลักของ Mithril

ผมก็เลยต้องถามว่าแล้วมันจะ get router state บน server ยังไงให้มัน thread safe เพราะถ้ากำลัง fetch data อยู่แล้วอีก request เข้ามา route มันก็จะเปลี่ยน

ปรากฎว่า ใน project ตัวอย่างมันใช้ Mithril router ใน clientside อย่างเดียว บน server มัน convert route definition ไปรันบน Express router

ก็เป็นประเด็นหนึ่งที่น่ากังวลถ้าต้องทำ server side rendering เพราะมันทำได้ แต่อาจจะต้อง if แยกไปพอสมควร สำหรับใน project นี้แล้วไม่มี server side rendering ก็เลยยังไม่ต้องห่วงเท่าไร

Component

เล่าทฤษฎีเสียนาน เข้าประเด็นดีกว่าว่า Component ใน Mithril หน้าตาอย่างไร

Component ใน Mithril เป็น JavaScript Object ธรรมดา ซึ่งก็น่าสนใจดีเพราะมันไม่ใช่ class ที่จะมี constructor และมันไม่ใช่ function ที่จะไม่มี state

const component = {
    view() {
        return m(".text", ["Hello world"]);
    }
}

ในทางปฏิบัติเหมือนว่า Mithril จะทำให้ component มี state อยู่ดีและเราสามารถเก็บข้อมูลไว้ใน this ได้ หรือใช้เรียกเมธอดอื่นๆ บน component

ฟังค์ชั่น JSX ของ Mithril คือ default export ของ Mithril เองซึ่งมักจะนิยมตั้งเป็น m

import m from 'mithril';

แต่ Mithril ไม่ค่อยจะเชียร์ JSX เท่าไรนักนอกจากบอกว่ามันก็ทำได้ เพราะ m จริงๆ นอกจากจะระบุ parameter แรกเป็น HTML Tag หรือ component ได้แล้วมันยังสามารถระบุ CSS Selector ได้ด้วย เรียกว่า Hyperscript เช่น m("input.input-lg[type=text][name=username]") จะได้ผลเป็น <input class="input-lg" type="text" name="username"> (กรณีที่มี dynamic attribute ก็ยังใส่ใน parameter ที่ 2 ของ m ได้ตามปกติเหมือน JSX ทั่วไป)

การใช้งานจริงผมพบว่า Prettier ไม่เข้าใจ Hyperscript และทำให้โค้ดอ่านค่อนข้างยาก เช่น โค้ดนี้

const component = {
    view() {
        return m("form.main", {
            onsubmit: onSubmit,
        }, [
            "Username: ",
            m("input[type=text][name=username]"),
            m("br"),
            m("input[type=submit][value=Submit]"),
        ]);
    }
}

จะโดน Prettier จัดเป็น

const component = {
    view() {
        return m(
            "form.main",
            {
                onsubmit: onSubmit,
            },
            [
                "Username: ",
                m("input[type=text][name=username]"),
                m("br"),
                m("input[type=submit][value=Submit]"),
            ]
        );
    },
};

ทำให้โค้ดค่อนข้างเทอะทะ อ่านยาก ทาง Prettier เองก็ไม่อยากให้มี Framework-specific formatting เท่าไรแต่อาจจะมีระบบ Plugin ในอนาคต

ในขณะเดียวกันเองถ้าจะเลี่ยงปัญหานี้ไปเขียน JSX เลย ก็ยังรู้สึกว่า m syntax สะดวกในการเขียนมากกว่า อย่างหนึ่งคือมันแยก static attribute กับ dynamic attribute ออกจากกันชัดเจน ก็ยังตัดสินใจไม่ค่อยได้เท่าไรว่าใช้แบบไหนดี ไม่อยากผสมกันหลายๆ ท่าเพราะมันจะเหนื่อยตอน convert เมื่อ component เริ่มเปลี่ยนไปแล้วอีกแบบเหมาะกว่า

Local State

ผมดองบล็อคเรื่องว่า React hook มันผิดยังไงมานานแล้ว สั้นๆ คือ Function ไม่มี State แต่ปรากฏว่า React hook call แต่ละครั้งได้ผลไม่เหมือนกัน แปลว่ามันต้องมี Global state สักที่

แล้ว React ก็ใส่ magic เข้าไป ทำให้ state แทนที่จะเป็น Global ก็ขึ้นอยู่กับ call site ได้ แต่เมื่อ call site เปลี่ยนมันก็พัง รวมถึงไปเรียกนอก React ไม่ได้

คำถามคือ แล้ว useState() ที่ไม่ Magic หน้าตายังไง? ผมก็ไม่รู้เหมือนกัน จนกระทั่งมาเห็นท่าที่ Mithril ทำ

function factory() {
    let counter = 0;

    return {
        view() {
            return m("div", [
                counter,
                m("input[type=button]", {
                    onclick: () => {
                        counter++
                    }
                })
            ])
        }
    }
}

ใช่! ใช่! ใช่!

วิธีที่เป็น Idiomatic ของ JavaScript ในการทำ local state ก็คือ Closure กับ class attribute นี่แหละ

เวลาใช้งาน function นี้เราไม่ต้องเรียก factory function เองแต่ Mithril ยอมรับ component factory เป็น component ด้วย แล้วมันจะไป manage instance ให้เอง

Final Thoughts

ผมคิดว่า Mithril ยังไม่ตอบโจทย์อยู่ 2 เรื่องที่ framework อื่นๆ ถือเป็นเรื่อง Hot มาก คือ Server rendering ซึ่งตามมาด้วยประเด็นว่า Global state ใน Frontend มันกลายเป็นอดีตไปแล้วเมื่อมันต้อง render บน client side

ถ้าตัด Server render ออกแล้วใช้ Global state คิดว่ามันก็น่าจะใช้ง่ายดี อาจจะตัด state management library ไปได้ด้วย แต่ถ้าทำแบบนั้นจะเอาไปเทียบกับเฟรมเวิร์คอื่นๆ ก็จะไม่แฟร์เท่าไร

Claim ที่แฟน Mithril เขียนมันผมก็เลยยังตามหาไม่เจอว่าแล้วมันจริงหรือเปล่า เค้ามองเห็นอะไรที่ผมยังไม่เจอหรือเปล่า และเค้าทำเว็บโดยไม่ใช้ SSR จริงหรอ

Hard things in VDOM Framework

หลังจากดู Framework มาหลายตัว คิดว่าจุดที่ควรจะศึกษาจาก Framework แต่ละตัวคือ

Routing

เท่าที่เขียนเว็บมา Routing จะมีอยู่สามแนวคือ

  1. Router เป็น component ที่ render subtree ตามหน้า ท่านี้จะเหมือนกับ React Router 4
  2. Router เป็น state manager แล้วเราต้อง check state และ render เอง ท่านี้เจอใน Router5
  3. Router อยู่ข้างนอกแล้ว Manage root component ตามที่ใช้ อันนี้เคยเขียนเองใน abstract-state-router หรือ Mithril router ก็จะทำท่านี้

ตอนนี้ยังเอนเอียงอยู่ระหว่าง 2 กับ 3 ว่าท่าไหนเป็นท่าที่ถูก ข้อดีของ Router5 คือมันทำ subroute แล้วให route หลัก render ส่วนนึง subroute render อีกส่วนได้ แต่ใน Vue อาจจะใช้ Slot API ทำท่านี้แทนได้โดยไม่ต้องพึ่ง router?

อีกปัญหาหนึ่งที่ยากของ Router คือ Fetch data ก่อนที่จะ Navigate ซึ่งเหมือนกับที่ Browser ทำอยู่ ก็แปลกใจมากที่ React Router 4 เลิก support ท่านี้ไปแล้วให้ไปทำที่ Link component แทน (กลายเป็นว่าคนใช้ต้องรู้ว่าจะ link ไปหน้านี้ต้องใช้ component ไหน) คิดว่า React suspense ออกมาเพื่อแก้ปัญหานี้ ส่วน Framework อื่นๆ ยังไม่ได้สนใจตรงนี้

State management

State ใน web framework เท่าที่เห็นจะมีอยู่สามแบบคือ

  1. Local state เช่น form เพราะถ้า hoist มันขึ้นมันจะช้า (แต่ก็มีคนทำอยู่บ้าง พวก Redux form)
  2. Scope state คือ local state แต่ pass through ลงไปให้ component ลูกด้วย บางที scope อาจจะใหญ่มากจนถึงทั้งหน้า
  3. Global state คือ state ที่ share ระหว่างหน้า หรือระหว่าง component ที่ไม่ได้เป็น parent-child กัน

ที่น่าจะดูคือเราสามารถ Pass object จากด้านบนลงไปด้านล่างแบบ implicit ได้ไหม (แบบ React context API) เพราะการใช้ global variable ไม่ดีต่อ server render ในขณะเดียวกันการ pass ทุกอย่างเป็น explicit ก็ทำให้ component ที่ไม่อ่าน state ก็ต้องรับ state ไปเพื่อส่งต่อด้วย (นอกจากนั้นอาจจะเจอปัญหาว่าข้างบนไม่ได้รับ state ข้างล่างจะใช้ไม่ได้)

บาง Framework อาจจะมี state management มาในตัวเลย (หรือมีตัวที่ค่อนข้างแนะนำ) อันนี้ก็ต้องลองดูเหมือนกันว่ามันเวิร์คมั้ย เช่น Angular $scope หรือ Vuex (ที่ไม่ได้มาด้วยกันแต่แนะนำมากๆ)

Reactivity

พูดถึง state แล้วสิ่งที่ตามมาคือเมื่อ state เปลี่ยนแล้วมันจะรู้ได้ยังไงว่าต้อง rerender อันนี้ด้านบนอธิบายไปแล้ว เลื่อนขึ้นไปอ่านได้

Code Splitting

อย่างน้อยๆ ก็ใน Router ก็มักจะได้ใช้ Code splitting แล้วใน Page อาจจะแยกต่อไปอีกก็ได้

บาง Framework เช่น Vue รองรับ component ที่คืน Promise มาได้เลย เห็นว่า React Suspense ก็น่าจะมา ส่วนบาง library ที่บ้านๆ มาหน่อยอาจจะต้อง manage Promise state เอง

ฟังดูเหมือนไม่ยาก แต่ถ้าวาด state machine มาจะพบว่ามันมีหลาย state กว่าที่คิด

  1. Initial (บาง framework เช่น React side effect จะเกิดขึ้นได้เฉพาะหลัง mount แล้ว)
  2. Loading แต่ไม่แสดง indicator
  3. Loading และแสดง indicator (ปกติถ้าโหลดเร็วมากๆ ไม่ขึ้น indicator เลย คนจะรู้สึกว่าเว็บเร็วกว่า)
  4. Load สำเร็จแล้ว ไปแสดง component จริง
  5. Load ไม่สำเร็จ แสดง error component

และบน server render มันอาจจะต้อง force ให้มันโหลด component ทันทีก่อนคืนให้ client

Fragment

บางที component ย่อย/output ของ loop อาจจะจำเป็นต้องคืนสมาชิกหลายตัวโดยไม่มีอะไรครอบ โดยเฉพาะเมื่ออยู่ภายใต้ <table> หรือ <ul> ตัว framework ควรจะอนุญาตให้คืนหลายๆ child component ได้

หรือบางที component อาจจะไม่อยาก render เลยถ้าไม่มีอะไร เช่น error display ถ้าไม่มี error ก็ไม่ต้องคืนอะไร

บาง library เช่น domvm ใช้แนวคิดว่า virtual DOM map 1-1 กับ DOM จริง ก็เลยจะไม่ support use case ข้างบนนี้ ต้องทำ div ขยะไว้บนหน้าแทน

Portal

โดยปกติแล้วในเว็บเราจะใช้ element ซ้อนกันไปได้เรื่อยๆ แล้วก็ใช้ CSS จัดหน้าได้ไม่มีปัญหา แต่ CSS เช่นกันที่ทำให้เราซ้อน element ที่ไม่เกี่ยวข้องกันไม่ได้เพราะ style ของ parent มีผลต่อ child ด้วย และบางอย่าง child override ไม่ได้ เช่น z-index ที่ยุ่งยาก ไปจนถึง position: relative แล้วมี position: absolute ซ้อนด้านใน

requirement ที่จะเจอบ่อยมากคือ Modal window ซึ่งในแอพง่ายๆ อาจจะยังไม่เจอหรือขี้เกียจเขียนแล้วใช้ prompt/alert ไปก่อน แต่พอจะทำดีๆ แล้ว framework บางตัวไม่มีคำตอบให้ว่าตอนนี้อยู่ข้างในจะโผล่ Modal ออกมาได้ยังไง

ท่า hackๆ หน่อย ก็อาจจะเป็นว่ามี component ตัวรับอยู่ด้านนอกแล้ว component ด้านในส่งสัญญาณไปหาตัวนอกว่าช่วย render อันนี้ตรงด้านนอกให้หน่อย ซึ่งมัน break การแยก component (ทำไม component ตัวนอกต้อง add portal receiver ที่ตัวเองไม่ได้ใช้) แล้วอาจจะมี hack เข้ามาเกี่ยวข้อง (component ด้านในคุยกับตัวด้านนอกยังไง? แล้วมันส่ง vdom ออกไปได้ยังไง?)

คำถามถัดมาคือแล้ว event bubbling จะทำงานยังไง? ถ้า parent ดัก click event ทั้งหมดไว้แล้วเราคลิกบน portal, parent ควรจะได้ไหม? เพราะ DOM จริงไม่ส่งมาเพราะมันไม่ได้เป็น parent-child กันแล้ว แต่ใน Virtual DOM มันยังเป็น parent-child กันอยู่นะ

CSS Class

คิดว่าการผสม CSS Class เป็นอะไรที่ใช้บ่อยมากถ้าใช้ CSS Module บาง framework เช่น Vue ก็ทำได้เลย แต่ถ้าใช้ React หรือ Mithril ก็ยังต้องใช้ classnames helper

บางคนก็จะใช้ CSS-in-JS ไปเลยซึ่งเวลาสั่งให้มันผสมมันจะทำ class ใหม่มา

Error Boundary

สุดท้ายเรื่อง Error ก็สำคัญกับ UX ที่ดี เราควรจะทำให้เว็บ partial failure ได้ว่าถ้า component ด้านในพัง ด้านนอกจะ render error ให้ user ได้ ซึ่งเราไม่สามารถ try..catch ใน render function ได้ (เพราะเราคืน Virtual DOM nodes ไปก่อน แล้ว vdom library เป็นคนเรียก component ด้านใน) ดังนั้น Virtual DOM ต้องจัดการเรื่อง error ให้ด้วยว่าถ้าข้างใน fail ต้องกลับมาให้ด้านนอก catch ได้อยู่