Home > Web Front-end > JS Tutorial > Build a Tiny React ChHooks

Build a Tiny React ChHooks

Linda Hamilton
Release: 2024-10-20 18:31:02
Original
837 people have browsed it

Build a Tiny React ChHooks

This tutorial is based on this tutorial, but with JSX, typescript and an easier approach to implement. You can checkout the notes and code on my GitHub repo.

Okay, before diving into hooks, we need a bit wrap up on the last chapter- still something to fix, but the last chapter was too much, so, well, here it is.

Fixing the last chapter

Here are some minor things- not entirely bugs, but better to fix them.

vDOM Comparison

In javascript, two functions are equal only if they are the same, stays unequal even if they have the same procedure, that is,

const a = () => 1;
const b = () => 1;
a === b; // false
Copy after login
Copy after login

So, when it comes to vDOM Comparison, we should skip the function comparison. Here is the fix,

for (let i = 0; i < aKeys.length; i++) {
    const key = aKeys[i]
    if (key === 'key') continue
    if (aProps[key] instanceof Function && bProps[key] instanceof Function) continue
    if (aProps[key] !== bProps[key]) return false
}
for (let i = 0; i < bKeys.length; i++) {
    const key = bKeys[i]
    if (key === 'key') continue
    if (aProps[key] instanceof Function && bProps[key] instanceof Function) continue
    if (aProps[key] !== bProps[key]) return false
}
Copy after login
Copy after login

Handling CSS

Style should be treated as a special property that is attributed to element with .style property. Here is the fix,

export type VDomAttributes = { 
    key?: string | number
    style?: object
    [_: string]: unknown | undefined
}

export function createDom(vDom: VDomNode): HTMLElement | Text {
    if (isElement(vDom)) {
        const element = document.createElement(vDom.tag)
        Object.entries(vDom.props ?? {}).forEach(([name, value]) => {
            if (value === undefined) return
            if (name === 'key') return
            if (name === 'style') {
                Object.entries(value as Record<string, unknown>).forEach(([styleName, styleValue]) => {
                    element.style[styleName as any] = styleValue as any
                })
                return
            }
            if (name.startsWith('on') && value instanceof Function) {
                element.addEventListener(name.slice(2).toLowerCase(), value as EventListener)
            } else {
                element.setAttribute(name, value?.toString() ?? "")
            }
        })
        return element
    } else {
        return document.createTextNode(vDom)
    }
}
Copy after login
Copy after login

Now these side fixes are done, let's move on to the main topic of this chapter- Hooks.

Capsuling vDOM Creation

We previously explicitly called render(vDom, app!), which requires creating vDOM by user, here is a better way to do it.

import { mount, useState, type FuncComponent } from "./runtime";
import { createElement, fragment, VDomAttributes, VDomNode } from "./v-dom";

const App: FuncComponent = (props: VDomAttributes, __: VDomNode[]) => {
    const [cnt, setCnt] = useState(0)
    return <div>
        <button onClick={() => setCnt(cnt() + 1)}>Click me</button>
        <p>Count: {cnt()}</p>
    </div>
}
const app = document.getElementById('app')
mount(App, {}, [], app!)
Copy after login
Copy after login
let reRender: () => void = () => {}
export function mount(app: FuncComponent, props: VDomAttributes, children: VDomNode[], parent: HTMLElement) {
    reRender = () => {
        const vDom = app(props, children) as unknown as VDomNode
        render(vDom, parent)
    }
    reRender()
}
Copy after login
Copy after login

Looks better more or less. Now let's move on to the main topic of this chapter- Hooks.

useState

Okay, let's get to the hook. The first hook we are going to implement is useState. It is a hook that allows us to manage the state of a component. We can have the following signature for useState,

Note that our implementation is slightly different from the original React. We are going to return a getter and a setter function, instead of returning the state directly.

function useState<T>(initialValue: T): [() => T, (newValue: T) => void] {
    // implementation
}
Copy after login

So where will we hook the value at? If we just hide it within the closure itself, the value will be lost when the component is re-rendered. If you insist on doing so, you need to access the space of the outer function, which is not possible in javascript.

So our way is to store it, you guessed it, in fiber. So, let's add a field to the fiber.

interface Fiber {
    parent: Fiber | null
    sibling: Fiber | null
    child: Fiber | null
    vDom: VDomNode
    dom: HTMLElement | Text | null
    alternate: Fiber | null
    committed: boolean
    hooks?: {
        state: unknown[]
    },
    hookIndex?: {
        state: number
    }
}
Copy after login

And we only mount hooks to the root fiber, so we can add the following line to the mount function.

export function render(vDom: VDomNode, parent: HTMLElement) {
    wip = {
        parent: null,
        sibling: null,
        child: null,
        vDom: vDom,
        dom: null,
        committed: false,
        alternate: oldFiber,
        hooks: oldFiber?.hooks ?? {
            state: []
        },
        hookIndex: {
            state: 0
        }
    }
    wipParent = parent
    nextUnitOfWork = wip
}
Copy after login

Hook index will come into use later. Now, hook index resets every time the component is re-rendered, but old hooks are carried over.

Please note that, we rendering component vDOM, only old fiber is accessible, so we can only manipulate that variable. However, it is null at the very beginning, so let's set up a dummy.

const a = () => 1;
const b = () => 1;
a === b; // false
Copy after login
Copy after login

Now we will have big brain time- since the order of each hook call is fixed (you can not use hooks in a loop or a condition, basic React rule, you know why it is now), so we can use safely use hookIndex to access the hook.

for (let i = 0; i < aKeys.length; i++) {
    const key = aKeys[i]
    if (key === 'key') continue
    if (aProps[key] instanceof Function && bProps[key] instanceof Function) continue
    if (aProps[key] !== bProps[key]) return false
}
for (let i = 0; i < bKeys.length; i++) {
    const key = bKeys[i]
    if (key === 'key') continue
    if (aProps[key] instanceof Function && bProps[key] instanceof Function) continue
    if (aProps[key] !== bProps[key]) return false
}
Copy after login
Copy after login

Well, let's try,

export type VDomAttributes = { 
    key?: string | number
    style?: object
    [_: string]: unknown | undefined
}

export function createDom(vDom: VDomNode): HTMLElement | Text {
    if (isElement(vDom)) {
        const element = document.createElement(vDom.tag)
        Object.entries(vDom.props ?? {}).forEach(([name, value]) => {
            if (value === undefined) return
            if (name === 'key') return
            if (name === 'style') {
                Object.entries(value as Record<string, unknown>).forEach(([styleName, styleValue]) => {
                    element.style[styleName as any] = styleValue as any
                })
                return
            }
            if (name.startsWith('on') && value instanceof Function) {
                element.addEventListener(name.slice(2).toLowerCase(), value as EventListener)
            } else {
                element.setAttribute(name, value?.toString() ?? "")
            }
        })
        return element
    } else {
        return document.createTextNode(vDom)
    }
}
Copy after login
Copy after login

It kind of works- the count increased from zero to one, but it does not increase further.

Well... strange right? Let's see what's going on, debug time.

import { mount, useState, type FuncComponent } from "./runtime";
import { createElement, fragment, VDomAttributes, VDomNode } from "./v-dom";

const App: FuncComponent = (props: VDomAttributes, __: VDomNode[]) => {
    const [cnt, setCnt] = useState(0)
    return <div>
        <button onClick={() => setCnt(cnt() + 1)}>Click me</button>
        <p>Count: {cnt()}</p>
    </div>
}
const app = document.getElementById('app')
mount(App, {}, [], app!)
Copy after login
Copy after login

You will see that, it always log 1. But the web page tells us that it is 1, so it should be 2. What's going on?

For native types, javascript passes by value, so the value is copied, not referenced. In React class component, it requires you to have a state object to address the issues. In functional component, the React addresses with, closure. But if we were to use the latter, it requires a big change in our code. So a simple way to get pass is, using function to get the state, so that the function will always return the latest state.

let reRender: () => void = () => {}
export function mount(app: FuncComponent, props: VDomAttributes, children: VDomNode[], parent: HTMLElement) {
    reRender = () => {
        const vDom = app(props, children) as unknown as VDomNode
        render(vDom, parent)
    }
    reRender()
}
Copy after login
Copy after login

And now, here we got it! It works! We created the useState hook for our tiny React.

Summary

Okay, you may believe that this chapter is too short- hooks are important to react, so why did we only implement useState?

First, many hooks are just variations of useState. Such hook is irrelevant to the component it is called, for example, useMemo. Such things are just trivial works and we got no time to waste.

But, second, the most important reason, is that, for hooks like useEffect, under our current, root-update based frame, they are nearly impossible to do. When a fiber unmount, you can not signal, because we only fetch the global vDOM and update the whole vDOM, whereas, in real React, it's not like so.

In real React, functional components are updated by the parent component, so the parent component can signal the child component to unmount. But in our case, we only update the root component, so we can not signal the child component to unmount.

However, the current small project have basically demonstrated how react works, and I hope you find it helpful in gaining a better understanding of the react framework.

The above is the detailed content of Build a Tiny React ChHooks. For more information, please follow other related articles on the PHP Chinese website!

source:dev.to
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Latest Articles by Author
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template