Skip to content

Instantly share code, notes, and snippets.

@rashkov
Last active September 24, 2020 16:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rashkov/f765917d09ebd629f385c21195f42b4c to your computer and use it in GitHub Desktop.
Save rashkov/f765917d09ebd629f385c21195f42b4c to your computer and use it in GitHub Desktop.
Implement React Hooks
const React = /* Implement this */;
  
function HelloComponent(){
    const [myNumber, setMyNumber] = React.useState(0);
    // Simulate rendering data to a screen
    console.log("My number:", myNumber);
    // Simulate rendering a clickable button
    const click = ()=> setMyNumber((num)=> num + 1);
    return click; 
}
  
React.mount(HelloComponent); // prints 0
React.simulateClick(HelloComponent); // prints 1
React.simulateClick(HelloComponent); // prints 2
React.simulateClick(HelloComponent); // prints 3

General idea being that the React item may be stateful, but the HelloComponent must be a stateless pure function.

How would you implement this?

@rashkov
Copy link
Author

rashkov commented Sep 3, 2020

SPOILER ALERT...


...


SOLUTION BELOW....





...



...



...




const React = new function(){
  this.state = {};
  this.mount = (component)=>{
    const compName = component.prototype.constructor.name;
    this.state[compName] = {
      render: component,
      state: [],
      hooks: [],
      initialRender: true,
      currentHookCall: 0,
      dom: null
    }

    // render the component for the first time
    this.state[compName].dom = component();
    this.state[compName].initialRender = false;
  }
  this.simulateClick = (component)=> {
    const compName = component.prototype.constructor.name;
    this.state[compName].dom();
  };
  this.useState = function(value){
    const compName = arguments.callee.caller.name;
    const compState = this.state[compName];
    let hook;
    if(compState.initialRender === true){
      // register hook
      hook = {
        id: compState.hooks.length,
        value: undefined,
        setValue: undefined
      };
      hook.value = value;
      hook.setValue = (newValFn)=>{
        hook.value = newValFn(hook.value);
        compState.render();
      };
      compState.hooks.push(hook);
    }else{
      const hookId = compState.currentHookCall % compState.hooks.length;
      compState.currentHookCall += 1;
      hook = compState.hooks[hookId];
    }
    return [hook.value, hook.setValue];
  }.bind(this)
};

function HelloComponent(){
  const [myNumber, setMyNumber] = React.useState(0);
  // Simulate rendering data to a screen
  console.log("My number:", myNumber);
  // Simulate rendering a clickable button
  const click = ()=>{
    setMyNumber((num)=> num + 1);
  };
  return click;
}

React.mount(HelloComponent); // prints 0
React.simulateClick(HelloComponent); // prints 1
React.simulateClick(HelloComponent); // prints 2
React.simulateClick(HelloComponent); // prints 3

@Macil
Copy link

Macil commented Sep 3, 2020

Checking the function callers stops hooks from being composable, and doesn't work in strict mode. React is the only thing that calls a component function, so React can just remember the component it's rendering before calling the function. Also, associating a mounted component with the component function itself prevents the same component from being mounted multiple times, so I tweaked the example to more closely match how React associates a mounted component root with a DOM node. Here's a solution that addresses that:

'use strict';

const React = (() => {
  const mountPoints = new Map();
  let renderingComponentSlot, currentHookCall;

  function internalRender(slot) {
    renderingComponentSlot = slot;
    currentHookCall = 0;
    const result = slot.component();
    renderingComponentSlot = undefined;
    return result;
  }

  return {
    mount(component, mountPoint) {
      const slot = {
        component,
        hooks: [],
        initialRender: true,
        dom: null
      };
      mountPoints.set(mountPoint, slot);
      slot.dom = internalRender(slot);
      slot.initialRender = false;
    },
    simulateClick(mountPoint) {
      const slot = mountPoints.get(mountPoint);
      slot.dom();
    },
    useState(value) {
      const slot = renderingComponentSlot;
      if (slot.initialRender) {
        const hook = [
          value,
          function setValue(newValFn) {
            hook[0] = newValFn(hook[0]);
            internalRender(slot);
          }
        ];
        slot.hooks.push(hook);
        return hook;
      } else {
        return slot.hooks[currentHookCall++];
      }
    },
  };
})();

function HelloComponent(){
  const [myNumber, setMyNumber] = React.useState(0);
  // Simulate rendering data to a screen
  console.log("My number:", myNumber);
  // Simulate rendering a clickable button
  const click = ()=>{
    setMyNumber((num)=> num + 1);
  };
  return click;
}

const mountPoint = {};

React.mount(HelloComponent, mountPoint); // prints 0
React.simulateClick(mountPoint); // prints 1
React.simulateClick(mountPoint); // prints 2
React.simulateClick(mountPoint); // prints 3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment