How to replicate useState with vanilla JS

Viewed 6793

What would be the implementation for the code in Vanilla JS that allows us to declare and update state like the way useState does so in React:

const [x, setX] = useState(12);
setX(14);
console.log(x); // 14

This question is strictly get better at JS. Naively it would make sense to go with:

// Solution 1

function update(value, newValue) {
    value = newValue;
    return value;
}

function state(value) {
    return [ value, update ];
}

let [value, setValue] = state(12)
value = setValue(value, 14)
console.log(value); // 14


// Solution 2

class State {
    constructor(value) {
        this.value = value;
    }
    
    update(newValue) {
        this.value = newValue;
    }
}

const x = new State(12);
x.update(14);
console.log(x.value); // 14

But I don't understand how the array [x, setX] has a callback (setX) that can affect x when declared with a const? I hope that makes sense.

3 Answers

I wanted to learn how to accomplish this as well. I found how to do it here. I refactored the code to use arrow functions instead, which can make the code snippet harder to read & understand. If it's the case, head to the resources shared in the link above.

This is the implementation:

const useState = (defaultValue) => {
  //  We create a function useState with a default value
  let value = defaultValue;
  //  We create a local variable value = defaultValue
  const getValue = () => value
  //  We create a function to set the value with parameter newValue
  const setValue = newValue => value = newValue //  We change the value for newValue
  return [getValue, setValue]; //  We return an array with the value and the function
}

const [counter, setCounter] = useState(0);
//  We destructure the array as a return of the useState function into two value

console.log(counter()); //  returns 0 which it's the value of counter()

I added the comments for easier understanding. This is the implementation withouth the comments:

const useState = (defaultValue) => {
  let value = defaultValue;
  const getValue = () => value
  const setValue = newValue => value = newValue
  return [getValue, setValue];
}

const [counter, setCounter] = useState(0);

console.log(counter());

For better reading & understanding, I have included the snippet using regular functions:

function useState(defaultValue) {
  let value = defaultValue

  function getValue() {
    return value
  }

  function setValue(newValue) {
    value = newValue
  }

  return [getValue, setValue];
}

const [counter, setCounter] = useState(0);

There is something very important you are missing - all react hooks use something "backing" them which allows you to provide what are effectively instance variables when you don't have an instance, you only have a function.

This thing in React is called a fiber and it effectively represents the lifecycle of a React component - it's not tied to the function itself, per se, it's tied to the component react is rendering (and re-rendering). Which is why you can have one functional component declaration, render that same function multiple times, and each of those will be able to maintain their own state - the state isn't part of the function, the state is part of the React fiber.

But I don't understand how the array [x, setX] has a callback (setX) that can affect x when declared with a const?

You aren't simply mutating the value of x when you call setX, what you are doing is telling React to re-render the component (fiber) with a new value for x.

EDIT:

A tremendously simplistic example, where the function itself is used as the backing instance of state (which is not the case in React) could look something like this:

// this line is example only so we can access the stateSetter external to the function
let stateSetter;

const states = new Map();

const useState = (value,context) => {
    const dispatch = v => {
            const currentState = states.get(context.callee);
            currentState[0] = typeof v === 'function' ? v(currentState[0]) : v        
            // we re-call the function with the same arguments it was originally called with - "re-rendering it" of sorts...
            context.callee.call(context);    
    }
    const current = states.get(context.callee) || [value,dispatch];
    states.set(context.callee,current);
    return current;

}

const MyFunction = function(value) {
    const [state,setState] = useState(value, arguments)
    stateSetter = setState;
    console.log('current value of state is: ',state)
}

MyFunction(10);
MyFunction(20); // state hasn't changed
stateSetter('new state'); // state has been updated!

A simple solution to mock the useState() using a constructor. This may not be the best solution as the constructor returns a copy of the function every time but achieves the problem in question.

function Hook(){
  return function (initialState){
    this.state = initialState;
    return [
      this.state,
      function(newState){
        this.state = newState;
      }
    ];
  }
}
const useState = new Hook();

Now, destructure the useState() which is an instace of Hook()

const [state, setState] = useState(0);
console.log(state); // 0
setState({x:20});
console.log(state); // { x: 20 }
setState({x:30});
console.log(state); // { x: 30 }
Related