DEV Community

Cover image for Creating a useState hook from scratch
Mikhail Kedzel for MiKi

Posted on

Creating a useState hook from scratch

In this article, we’ll create the most barebones version of the useState hook. If you want to understand how React hooks work in depth, how they use closures, and why can’t you nest hooks in if blocks - read ahead!

First of all, you probably know that React doesn’t need JSX, and in the end, it transforms everything to React.createElement(component, props, ...children) . You can read more about it in the official docs https://reactjs.org/docs/react-without-jsx.html

With this in mind, to not overcomplicate ourselves, let’s create just a .ts file called index.ts. Then, we can add our simple custom component without any JSX.

function App() {
  const [count, setCount] = useState(0);

  return {
    count: `Count is ${count}`,
    setCount
  };
}
Enter fullscreen mode Exit fullscreen mode

To emulate rendering and re-rendering, we’ll do the same thing that React does - just call the function. Then we’ll get our count and log it to see what’s there.

function App() {
  const [count, setCount] = useState(0);

  return {
    count: `Count is ${count}`,
    setCount
  };
}

const Rendered = App();
console.log(Rendered.count);
Enter fullscreen mode Exit fullscreen mode

Oh, right, useState is not defined… Let’s create it!

We need our function to accept initialState and return an array of two items - state and a function to modify it.

function useState(initialState?: any) {
  let state = initialState ?? undefined;

  return [state, (newState: any) => (state = newState)];
}
Enter fullscreen mode Exit fullscreen mode

Let’s run our code again

console output

We’re getting somewhere :). But let’s try to change our state

let Rendered = App();
console.log(Rendered.count);
Rendered.setCount(10);
Rendered = App() //re-render after state change
console.log(Rendered.count);
Enter fullscreen mode Exit fullscreen mode

Console output

Same thing. That’s because our useState function gets re-created on every re-render

We can use closures to our advantage here and lift the state up to be a global variable not to get re-created on every re-render.

let state: any;

function useState(initialState?: any) {
  /* We want to check if state is undefined, i.e. it's the first render
     before assigning initialState to it.
  */
  if(typeof state === 'undefined') state = initialState;

  return [state, (newState: any) => (state = newState)];
}
Enter fullscreen mode Exit fullscreen mode

console output

Much better, but how do we go about creating two states? Three states?

function App() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState("John");

  return {
    count: `Count is ${count}`,
    setCount,
    name: `Name is ${name}`,
    setName
  };
}

let Rendered = App();
console.log(Rendered.count);
console.log(Rendered.name);
Rendered.setCount(10);
Rendered.setName("Mark");
Rendered = App(); //re-render after state change
console.log(Rendered.count);
console.log(Rendered.name);
Enter fullscreen mode Exit fullscreen mode

console ouput

Since we only have one global state variable, our two useStates have to share it, which leads to incorrect behavior.

The way React implements it - is by having a state array and a pointer to know which useState we are currently on. After the useState function is fired, it increments this pointer so that the next useState function will work with its state, and so on. After a re-render, since our App() and all the useState functions are called again, we need to set this pointer to 0. Can you already guess why nesting a useState in an if statement is bad?

let state: any[] = [];
let index = 0;

function useState(initialState?: any) {
  // Freeze the index, to make the 'setters'(setCount/setName) work
  const localIndex = index;
  if (typeof state[localIndex] === 'undefined') state[localIndex] = initialState;

  index++;
  return [state[localIndex], (newState: any) => (state[localIndex] = newState)];
}

function App() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState("John");

  return {
    count: `Count is ${count}`,
    setCount,
    name: `Name is ${name}`,
    setName
  };
}

let Rendered = App();
console.log(Rendered.count);
console.log(Rendered.name);
Rendered.setCount(10);
Rendered.setName("Mark");
index = 0; //Set the index to 0
Rendered = App(); //re-render after state change
console.log(Rendered.count);
console.log(Rendered.name);
Enter fullscreen mode Exit fullscreen mode

One more trick we are doing here is “freezing” the index. Otherwise, when we get to the “name” useState, it’ll increment the index to 1, and setCount will end up changing the name instead of count. Because name has an index of 1 and count has an index of 0

Let’s see where it got us!

console output

Works perfectly!

Conclusion

Of course, having built this we are still far of from becoming a React maintainer at Facebook :) But the best way to learn something is by doing it. So here are a couple of ways you can improve this on your own

  • Instead of manually setting index to 0 before calling our function every time, encapsulate this into a render method which’d do both.
  • Store your custom React in an IIFE instead of globally in our index.ts file, just like React does it https://developer.mozilla.org/en-US/docs/Glossary/IIFE
  • Implement useEffect
  • Use your imagination and implement more stuff!

This article was inspired by two great talks:

https://www.youtube.com/watch?v=KJP1E-Y-xyo

https://www.youtube.com/watch?v=f2mMOiCSj5c

Thank you for reading!


At MiKi(https://miki.digital/) we help businesses make stunning websites with a blazing-fast and cost-efficient cloud backend.
Interested?
Feel free to give us a call at +447588739366, book a meeting at https://calendly.com/miki-digital/30-minute-consultation or feel out the contact form at our website https://www.miki.digital/contact

Top comments (1)

Collapse
 
shahjalalbu profile image
Md Shahjalal

State not updated😪