1 introduction

This article will show you how to implement a react.js in pure JavaScript in 50 lines of code without relying on any third party libraries.

The purpose of this article is to strip away the componentized form of react. js that may be difficult for beginners to understand and give you more energy and attention to learn the essence of React.js. If you’re just starting to learn React. Js and feeling confused, this article should clear up some of the confusion.

Also note that the code implemented in this article is intended for instructional presentations only and is not intended for production environments. The code hosts the repository. If you’re in a hurry, you can look at the code first, but this article will start with the basics.

2 It all starts with thumbs up

The rest of the code will evolve from a basic “like” function, and you’ll see the article code start to look more and more like the react.js component code. In this process, you only need to follow the ideas of the article, you can experience the form of componentization in the evolution of the code.

Suppose we now need to implement a “like” and “unlike” feature.

[image:B4B41FF2-519A-4A7C-8035-0D5CD4EE8FFA-86900-00013723B2CAE361/8D274601-162D-4B36-B1E0-9C65FB0C494F.png]

If you know a little bit about the front end, you can pick it up:

HTML:

<body> <div class='wrapper'> <button class='like-btn'> <span class='like-text'> </span> <span>👍</span> </span> </div> </body>Copy the code

It is easy to make the HTML structure of the button a little more complicated for the sake of real-world situations. With this HTML structure, now add some JavaScript behavior to it:

JavaScript:

const button = document.querySelector('.like-btn') const buttonText = button.querySelector('.like-text') let isLiked = false button.addEventListener('click', function () { isLiked = ! IsLiked if (isLiked) {buttontext.innerhtml = 'cancel'} else {buttontext.innerhtml = 'like'}}, false)Copy the code

The function and implementation are very simple, the button can already provide like and unlike function. Your co-worker comes up and says he loves your button and wants to use the “like” feature you wrote. You’ll find this implementation deadly: your colleague will have to copy the entire button and its structure, as well as the entire JavaScript code. There is no reusability in such an implementation.

3 Achieve reusability

So let’s figure out a way to make this feature reusable so that your colleagues can use it with ease.

3.1 Structure reuse

Now let’s rewrite the like function. This time we’ll start with a class with a render method that returns a string representing an HTML structure:

Class LikeButton {render () {return '<button id='like-btn'> <span class=' like-btn'> <span>👍</span> </button> ` } }Copy the code

You can then use this class to build instances of different likes and plug them into the page.

  const wrapper = document.querySelector('.wrapper')
  const likeButton1 = new LikeButton()
  wrapper.innerHTML = likeButton1.render()

  const likeButton2 = new LikeButton()
  wrapper.innerHTML += likeButton2.render()
Copy the code

[image:4AEFC6B6-F913-440E-9306-CCC454A7A30C-87312-00013B98FB6F8354/4555573C-8435-4079-9D64-C76913AB6E40.png]

This is a very violent use of innerHTML, with two buttons roughly inserted into the wrapper. Although you may be very unhappy with this implementation, we managed to reuse the structure. We’ll optimize it later.

3.2 Generate DOM elements and add events

You’ll notice, of course, that the button is dead, and it doesn’t react when you click on it. Because there are no events added to it at all. But the problem with the LikeButton class is that there’s a button there, but it’s actually inside the string. How can you add events to a string? The API for DOM events is only available for DOM constructs.

We need the DOM structure, to be exact: we need the DOM structure represented by the HTML string of the “like” function. Suppose we now have a function called createDOMFromString, and you pass in an HTML string to that function, but it will return the corresponding DOM element to you. This problem can be solved.

// ::String => ::Document
const createDOMFromString = (domString) => {
  // TODO 
}
Copy the code

Let’s not worry about how this function should be implemented, but what it does. Use it to rewrite the LikeButton class:

class LikeButton { render () { this.el = createDOMFromString(` <button class='like-button'> <span Class = 'like - text > thumb up < / span > < span > 👍 < / span > < / button > `) this. El. AddEventListener (' click' () = > console. The log (' click '), false) return this.el } }Copy the code

Instead of an HTML string, render() now returns a DOM generated from the HTML string. An event is added to the DOM element before it is returned.

Because render now returns a DOM element, you can’t use innerHTML to violently insert a wrapper. I’m going to plug it in using the DOM API.

    const wrapper = document.querySelector('.wrapper')

  const likeButton1 = new LikeButton()
  wrapper.appendChild(likeButton1.render())

  const likeButton2 = new LikeButton()
  wrapper.appendChild(likeButton2.render())
Copy the code

Now you click on both buttons, and each button prints click on the console, indicating that the event binding is successful. But the text on the buttons still doesn’t change, and the LikeButton code can complete the function with a few tweaks:

class LikeButton { constructor () { this.state = { isLiked: false } } changeLikeText () { const likeText = this.el.querySelector('.like-text') this.state.isLiked = ! If (this.state. IsLiked) {likeText. InnerHTML = 'like'} else {likeText. {this.el = createDOMFromString(' <button class='like-button'> <span class='like-text'> <span>👍</span> </button> `) this.el.addEventListener('click', this.changeLikeText.bind(this), false) return this.el } }Copy the code

The code here is a little longer, but it’s easy to understand. The LikeButton constructor adds an object state to each instance of the LikeButton, which holds the status of whether or not each button is liked. We’ve also rewritten the original event-binding function: once click was printed, the button is now clicked with a changeLikeText method that changes the text of the like button based on the state of this.state.

If you’re still following this thread, notice that the code now looks a bit like the react.js component code. But we didn’t say anything about React. Js at all, we were all about “componentization”.

Now that this component is quite reusable, your colleagues just need to instantiate it and insert it into the DOM.

4 Why not be violent?

Take a closer look at the changeLikeText function, which contains the DOM manipulation, and looks simpler now because there is now only one state, isLiked. But think about it, you need to update the content of the page because your data state changes, so if your component contains a lot of state, then your component is basically all DOM manipulation. It’s not uncommon for a component to contain a lot of state, so there’s room for optimization: How do you minimize this manual DOM manipulation?

4.1 State changes -> Build a new DOM element

One solution proposed here is to re-call the Render method as soon as the state changes and build a new DOM element. What are the benefits of this? The nice thing is that you can use the latest this.state in the Render method to construct different HTML structured strings, and construct different DOM elements from that string. The page is updated! If this sounds a bit convoluted, let’s look at the code:

class LikeButton { constructor () { this.state = { isLiked: false } } setState (state) { this.state = state this.el = this.render() } changeLikeText () { this.setState({ isLiked: ! this.state.isLiked }) } render () { this.el = createDOMFromString(` <button class='like-btn'> <span class='like-text'>${this.state.isLiked ? 'Cancel' : </span> <span>👍</span> </button> ') this.el.addeventListener ('click', this.changeliketext.bind (this), false) return this.el } }Copy the code

Just a few minor changes:

  1. renderThe HTML string inside the function is based onthis.stateDifferent is different (this is using ES6’s string feature, which is handy for this kind of thing).
  2. A newsetStateFunction, which takes an object as an argument; It will set the instancestateAnd then call it againrenderMethods.
  3. When the user clicks the button,changeLikeTextWill build a newstateObject, this new onestateAnd the incomingsetStateDelta function.

As a result, every time the user clicks, changeLikeText calls change component state and then setState; SetState calls the Render method to rebuild the new DOM element; The Render method builds different DOM elements depending on the state.

That is, you just call setState and the component will be rerendered. We have successfully eliminated unnecessary DOM manipulation.

4.2 Re-insert a new DOM element

The above improvements won’t work because if you look closely, you’ll notice that the DOM element being re-rendered is not actually inserted into the page. So in addition to this component, you need to know that the component has changed and that the new DOM element has been updated to the page.

Modify the setState method:

. setState (state) { const oldEl = this.el this.state = state this.el = this.render() if (this.onStateChange) this.onStateChange(oldEl, this.el) } ...Copy the code

When using this component:

Const likeButton = new likeButton () wrapper. AppendChild (likeButton.render()) // Insert DOM element component.onStatechange = for the first time (oldEl, newEl) => {wrapper.insertbefore (newEl, oldEl) // Insert new element wrapper.removechild (oldEl) // Delete old element}Copy the code

The onStateChange method is called every time setState is instantiated, so you can customize the behavior of onStateChange. So what we’re doing here is whenever we setState, we’re inserting a new DOM element, and then we’re deleting the old one, and the page is updated. This has been further optimized: you no longer need to manually update the page.

Unusual violence. No matter, such violence can be circumvented by virtual-dom’s diff strategy, but that is beyond the scope of this article.

This version has nice likes, and I can keep adding features to it without having to manually manipulate the DOM. But the downside is that if I were to create a new component, like a comment component, then I would have to rewrite all of these setState methods, and I could actually pull them out.

5. Abstract the Component class

To make the code more flexible and write more components, I abstracted this pattern into a Component class:

  class Component {
    constructor (props = {}) {
      this.props = props
    }

    setState (state) {
      const oldEl = this.el
      this.state = state
      this.el = this.renderDOM()
      if (this.onStateChange) this.onStateChange(oldEl, this.el)
    }

    renderDOM () {
      this.el = createDOMFromString(this.render())
      if (this.onClick) {
        this.el.addEventListener('click', this.onClick.bind(this), false)
      }
      return this.el
    }
  }
Copy the code

There is an additional mount method, which inserts the component’s DOM element into the page and updates the page with setState:

  const mount = (wrapper, component) => {
    wrapper.appendChild(component.renderDOM())
    component.onStateChange = (oldEl, newEl) => {
      wrapper.insertBefore(newEl, oldEl)
      wrapper.removeChild(oldEl)
    }
  }
Copy the code

In this case, if we rewrite the likes component, it will look like:

class LikeButton extends Component { constructor (props) { super(props) this.state = { isLiked: false } } onClick () { this.setState({ isLiked: ! this.state.isLiked }) } render () { return ` <button class='like-btn'> <span class='like-text'>${this.props.word || ''} ${this.state.isLiked ? 'cancel' : 'thumb up'} < / span > < span > 👍 < / span > < / button > `}} the mount (wrapper, new LikeButton ({word: 'hello'}))Copy the code

Have you noticed that your code is already similar to the react.js component? It’s still working code, and we use pure JavaScript from start to finish without relying on any third party libraries. (Note the addition of props, not mentioned above, to pass configuration properties to components, just like react.js).

With the above Component class and the mount method, it takes less than 40 lines of code to componentize. If we need to write another Component, we simply inherit the Component class as above:

class RedBlueButton extends Component { constructor (props) { super(props) this.state = { color: 'red' } } onClick () { this.setState({ color: 'blue' }) } render () { return ` <div style='color: ${this.state.color}; '>${this.state.color}</div> ` } }Copy the code

Easy to use, the full code can be found here: the repository

Oh, there’s also the mysterious createDOMFromString, which is even simpler:

  const createDOMFromString = (domString) => {
    const div = document.createElement('div')
    div.innerHTML = domString
    return div
  }
Copy the code

6 summarizes

What exactly do you get out of the article?

Ok, I admit I’m title-mad, this 40-line code is a crippled and mentally retarded version of React. Js, no JSX, no component nesting, etc. It is simply an implementation of the componentized representation of React. Js. It doesn’t touch the essence of React. Js at all.

In fact, the essence of React. Js is probably its Virtual DOM algorithm. Its setState, props, and so on are just a form, and many beginners will be confused by this form. This article actually reveals the realization principle of this componentized form. If you’re learning or confused about React. Js, this article should clear up some confusion.

This article does not involve any content of Virtual DOM, students who need to refer to this blog, the introduction is very detailed. If you are interested, you can combine the two and implement a react.js using Virtual DOM instead of mount.

If you have any doubts about the content of this article, please follow my Zhihu column and comment or send me a private message to Zhihu.