Recreating the React workflow in vanilla JavaScript
Tags: javascript, react, webdev, typescript
Published: 2020-11-27 11:15:13

Recently, I have been experimenting with different approaches for building vanilla JavaScript apps. And I got the idea to recreate basic React functionality in order to get a similar workflow as in React. This would enable me to keep the benefits of vanilla JavaScript while having the structure of React apps. It would also make it easy to migrate code into React if the app grows.

By the end of this post, I will show you how to make a counter component with code that looks almost identical to React code, without using any React. As can be seen here:
```tsx
import * as elements from 'typed-html';
import { notReact } from '../notReact';

const Counter = () => {
  const [count, setCount] = notReact.useState(0);
  
  const increaseCounter = () => {
    setCount(count+1);
  }
  notReact.addOnClick("increaseCount", increaseCounter);
  
  let isHigherThan5: string;
  notReact.useEffect(()=>{
    isHigherThan5 =  count > 5 ? "Yes" : "No";
  }, [count, isHigherThan5]);
  return (
    <div>
      <h1>Counter: {count}</h1>
      <button id="increaseCount">Increase count</button>
      <p>Is the count higher than 5? <strong>{isHigherThan5}!</strong></p>
    </div>
  );
}

export default Counter;
```

You can find the repository [here](https://github.com/maturc/notReact).


# Setup
The first thing that I did was that I installed webpack and typescript. The main reason why I'm using typescript is because it makes it easy to use jsx, otherwise it's not mandatory. The same could likely be done with babel as well.

After a standard webpack and typescript installation, I installed [typed-html](https://github.com/nicojs/typed-html) `npm install --save typed-html`. This is a package that lets us use jsx inside of typescript `tsx` files.
After it was installed, I added the following lines into the typescript config file.
```js
{
  "compilerOptions": {
    "jsx": "react",
    "jsxFactory": "elements.createElement",
  }
}
```
This factory comes with some limitations.
```jsx
<foo></foo>; // => Error: Property 'foo' does not exist on type 'JSX.IntrinsicElements'.
<a foo="bar"></a>; // => Error:  Property 'foo' does not exist on type 'HtmlAnchorTag'
```
We can't use props and components like we usually would in React, instead, a component will be a function call and the function arguments will be props.

Now, what does the jsx factory even do?
It transpiles the jsx into a string. That works for me, because I wanted to do the rending with a simple `.innerHTML`. But if you want to get some other kind of output, you could use some other factory or even make your own.
You could also avoid using jsx and just use template literals instead.

Before I started coding I also had to create an index.html file.
/public/index.html
```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <title>App</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <script src="bundle.js"></script>
  </body>
</html>
```

# Rendering

Now that everything was set up, it was time to dive into JavaScript.

First I made a file called `notReact.ts` and put it inside of the `/src` folder. This file is where all of the rendering and state logic was located in.
First I made a function closure and put two functions inside of it. One for initialization and one for rendering.
```ts
export const notReact = (function() {
  let _root: Element;
  let _templateCallback: ITemplateCallback;

  function init(rootElement: Element, templateCallback: ITemplateCallback) {
    _root = rootElement;
    _templateCallback = templateCallback;
    render();
  }
  function render() {
    _root.innerHTML = _templateCallback();
  }
  
  return {init, render};
})();


type ITemplateCallback = { (): string; }
```
`init()` has two arguments, a root element that will be used as a template container and a callback function that returns a string, containing all of the html.
The `render()` function calls the template callback and assigns it to the `.innerHTML` of the root element.

Next, I made the `index.ts` and the `App.tsx` file and put both of them inside of the `/src` folder.

Then I initialized the rendering and called the `App` component inside of the `index.ts` file.
```ts
import App from "./App";
import { notReact } from "./notReact";

const render = () => {
  const root = document.getElementById('root');
  notReact.init(root, App);
}

window.addEventListener("DOMContentLoaded", () => render());
```

Inside of the `App` component I wrote a simple "Hello world".
```tsx
import * as elements from 'typed-html';

const App = () => {
  return (
    <h1>
      Hello world;
    </h1>
  );
}

export default App;
```
Here is the result:
![result](https://dev-to-uploads.s3.amazonaws.com/i/4z216v62i3kcmsri5bhb.png)

# State and event listeners

Now that the rendering was done, it was time to write the `useState` hook, while also creating a basic counter application to test it out.
First I created another component called `Counter.tsx` and put it inside of the `components` folder.
I wrote it the same way it would be written in regular React, with the exception of the `onClick` event that I omitted for now.
```tsx
import * as elements from 'typed-html';
import { notReact } from '../notReact';

const Counter = () => {
  const [count, setCount] = notReact.useState(0);
  
  const increaseCounter = () => {
    setCount(count+1);
  }
  return (
    <div>
      <h1>Counter: {count}</h1>
      <button>Increase count</button>
    </div>
  );
}

export default Counter;
```
After that, I had to change the App component:
```tsx
import * as elements from 'typed-html';
import Counter from './components/Counter';

const App = () => {
  return (
    <div>
      {Counter()}
    </div>
  );
}

export default App;
```
With everything being ready, it was time to write the useState hook.
```ts
export const notReact = (function() {
  let hooks: Array<any> = [];
  let idx: number = 0;

  function useState(initValue: any) {
    let state;
    state = hooks[idx] !== undefined ? hooks[idx] : initValue;
    const _idx = idx;
    const setState = (newValue: any) => {
      hooks[_idx] = newValue;
      render();
    }
    idx++;
    return [state, setState];
  }
  function render() {
    idx=0; //resets on rerender
    ...
  }
  return {useState, init, render};
})();
```
There are two local variables. An array variable called `hooks` that contains all of the state values. And the `idx` variable which is the index used to iterate over the `hooks` array.

Inside of the `useState()` function, a state value together with a setter function get returned for each `useState()` call.

Now we have a working `useState` hook, but we can't test it out yet. We need to add an `onclick` event listener to the button first. The problem here is that if we add it directly into the jsx, the function will be undefined because of the way the html is being rendered here.
To fix this, I had to update the `notReact.ts` file again.
```ts
export const notReact = (function() {
  const _eventArray: IEventArray = [];

  function render() {
    _eventArray.length = 0; //the array gets emptied on rerender
    ...
  document.addEventListener('click', (e) => handleEventListeners(e));
  function handleEventListeners(e: any) {
    _eventArray.forEach((target: any) => {
      if (e.target.id === target.id) {
        e.preventDefault();
        target.callback();
      }
    });
  }
  function addOnClick(id: string, callback: any) {
    _eventArray.push({id, callback});
  }
  return {useState, useEffect, init, render, addOnClick};
})();

type IEventArray = [{id: string, callback: any}] | Array<any>;
```
I made a local variable named `eventArray`. It's an array of objects, containing all elements that have an `onclick` event, together with a callback function for each of those events.
The `document` has an `onclick` event listener. On each click it checks if the target element is equal to one of the event array elements. If it is, it fires it's callback function.

Now let's update the Counter component so that the button has an onclick event:
```tsx
const Counter = () => {
  ...
  notReact.addOnClick("increaseCount", increaseCounter);
  ...
  return (
    <div>
      <h1>Counter: {count}</h1>
      <button id="increaseCount">Increase count</button>
    </div>
  );
}
```
Here is the result so far:
![counter demo](https://dev-to-uploads.s3.amazonaws.com/i/2jygc0by93ci95h2y5ec.gif)

# Side effects
The last thing that I added was the `useEffect` hook.
Here is the code:
```tsx
export const notReact = (function() {
  let hooks: Array<any> = [];
  let idx: number = 0;

  function useEffect(callback: any, dependancyArray: Array<any>) {
    const oldDependancies = hooks[idx];
    let hasChanged = true;
    if (oldDependancies) {
      hasChanged = dependancyArray.some((dep, i) => !Object.is(dep, oldDependancies[i]));
    }
    hooks[idx] = dependancyArray;
    idx++;
    if (hasChanged) callback();
  }
  
  return {useState, useEffect, init, render, addOnClick};
})();
```
It saves dependancies from the last render and checks if they changed. If they did change the callback function gets called.

Lets try it in action! I added a message bellow the button that changes if the counter gets higher than 5.
Here is the final counter Component code:
```jsx
import * as elements from 'typed-html';
import { notReact } from '../notReact';

const Counter = () => {
  const [count, setCount] = notReact.useState(0);
  
  const increaseCounter = () => {
    setCount(count+1);
  }
  notReact.addOnClick("increaseCount", increaseCounter);
  
  let isHigherThan5: string;
  notReact.useEffect(()=>{
    isHigherThan5 =  count > 5 ? "Yes" : "No";
  }, [count, isHigherThan5]);
  return (
    <div>
      <h1>Counter: {count}</h1>
      <button id="increaseCount">Increase count</button>
      <p>Is the count higher than 5? <strong>{isHigherThan5}!</strong></p>
    </div>
  );
}

export default Counter;
```
![final demo](https://dev-to-uploads.s3.amazonaws.com/i/wcf2atu0yeu1vo7cz53l.gif)

# Conclusion
This is it! The component is looking a lot like actual React now. Changing it for React would be trivial, the only thing that would need to be changed is the `onclick` event and the imports.

If you enjoy working in React, an approach like this might be worth trying out. Just keep in mind that this code is **proof of concept**, it isn't very tested and there are definitely a lot of bugs, especially when there are a lot of different states. The code has a lot of room for improvement and expansion. It's not a lot of code though, so it would be easy to change it based on your project requirements. For a more serious application, you would most likely have to implement some sort of event loop that would synchronize the state changes.

I didn't go very in depth about my implementation of the useState and useEffect hooks. But if you want more details, you can check out this [talk](https://www.youtube.com/watch?v=KJP1E-Y-xyo), it's what inspired my implementation.

Again, all of the code can be found in [this repository](https://github.com/maturc/notReact).

**Thank you for reading!** 😁