Why you have to use className in React, but not in Preact?

0
34
Why you have to use className in React, but not in Preact?


State of things

We all know this simple rule. Use className instead of class if you write JSX.

const ExampleReactComponent = () => {
    return <div className="foo">Example React component</div>
}

React docs warns us about this convention straightaway. And it goes even further, you need to specify all HTML attributes in camelCase.

Okay, if it’s the way things work, we can get used to it. Since JSX is different from HTML in a lot of ways, it’s somewhat justifiable.

Wait a moment. We actually can use class in JSX, but only if we would use Preact instead of React.

const ExamplePreactComponent = () => {
    return <div class="foo">Example Preact Component</div>
}

And it’s a legitimate documented feature, not a coincidence or a bug. So, the question is – why?
Why do we have to camelCase all HTML attributes in React, but not in Preact?

Disclaimer: If you aren’t familiar with JSX, but want to read and understand this article anyway, check out my other article, where we take a look at what JSX is and how it works under the hood.

The reason behind the rule

First thing first, let’s clearly define the reason behind this rule in React.
The official React docs have a quite vague explanation.

Since JSX is closer to JavaScript than to HTML, React DOM uses camelCase property naming convention instead of HTML attribute names.

It’s hard to say solely from this explanation what the real reason is.
So, let’s google it and try to find more info!

It’s a reserved keyword

There is an article about this problem on GeeksForGeeks. Let’s consider an explanation from it.

The only reason behind the fact that it uses className over class is that the class is a reserved keyword in JavaScript and since we use JSX in React which itself is the extension of JavaScript, we have to use className instead of the class attribute.

First of all, yeah, technically speaking class is a reserved keyword in JavaScript for making, so-called, class declarations like this one.

class Polygon {
  constructor(height, width) {
    this.area = height * width;
  }
}

But we actually can use class keyword in JavaScript without much trouble.

const obj = {
    class: 'value'
};

const otherObj = {};

otherObj.class = 'value';

You may think, it didn’t work last time I checked! And you’ll be right.
This works only in modern versions of JavaScript. So that’s the point? Not exactly.
In older versions of JavaScript, you may easily achieve the same thing by explicitly turning the class property into a string literal like so.

const obj = {
    'class': 'value'
};

const otherObj = {};

otherObj['class'] = 'value';

Okay, maybe the real reason is separate from this whole reserved-keyword issue. Maybe, it’s the JSX itself!

It’s a JSX-specific issue

Just think about it. JSX is an extension of JavaScript, not one-to-one clone or so. That’s why even though it’s tightly coupled with JS, it may propose some other restrictions.

Let’s battle-test this theory. We’ll declare a simple component with a className attribute.

const example = <div className="foo">Hello world!</div>

Then, we’ll put it through Babel transpiler.

const example = React.createElement("div", {
  className: "foo"
}, "Hello world!");

Live example in Babel REPL, in case you want to check yourself.

The result is pretty much expected and fully valid. Now let’s try another one. Let’s use class instead of className in this try.

const example = <div class="foo">Hello world!</div>

And after transpilation we get this.

const example = React.createElement("div", {
  class: "foo"
}, "Hello world!");

Live example of this try in Babel REPL.

First of all, it’s fully valid, as well as, the former one.
Secondly, Babel transpiles this snippet, like it was nothing new or weird for him. So, it seems like JSX isn’t an issue either.

Okay, maybe we’ll face some issues in the render phase. Because JSX in itself is just syntax and it doesn’t create UI on its own. We need to render JSX somewhere to see the end UI. So we’ll try to do exactly that to see, if some problems may arise.

It’s a render function problem

Let’s create a simple render function from scratch because obviously React won’t allow us to use its render mechanism with class instead of className.
Our render function will render the result of React.createElement to the DOM. But what does the result of React.createElement look like?
React.createElement returns, so-called, virtual node.
It looks like this in our case.

const example = {
    $$typeof: Symbol(react.element),
    key: null,
    ref: null,
    props: {
        class: "foo"
    },
    type: "div",
    children: ["Hello world!"],
    _owner: null
}

But what is a virtual node anyway?
Virtual node or vnode, in short, is just a lightweight representation of a given UI structure. In the case of the browser, the virtual node represents the real DOM node. React uses virtual nodes to construct and maintain, so-called, virtual DOM, which itself is a representation of real DOM.

Sidenote: If you want to dig into this whole virtual madness, let me know in the comments and I’ll make an article, where we’ll go through the whole concept of virtual DOM and make our own implementation of it.

To implement the render function and check how things work, we only need three basic properties of the vnode.

const example = {
    
    type: "div",
    
    props: {
        class: "foo"
    },
    
    children: ["Hello world!"],
}

Sidenote: If you want to understand what other properties are and why they are here, let me know in the comments section and I’ll make detailed articles with a deep explanation of each individual property.

Now with new knowledge we are fully ready to create our own render function for vnode tree.
Let’s start with the basics and create element of the passed type.

const render = (vnode) => {
    const el = document.createElement(vnode.type);
    return el;
}

Then let’s handle the props.

const render = (vnode) => {
    const el = document.createElement(vnode.type);

    const props = vnode.props || {};  
    Object.keys(props).forEach(key => {
        el.setAttribute(key, props[key]);
    });

    return el;
}

Next, let’s recursively add our children and handle edge-case, in which a child is a string.

const render = (vnode) => {
    if (typeof vnode === 'string') return document.createTextNode(vnode);

    const el = document.createElement(vnode.type);

    const props = vnode.props || {};  
    Object.keys(props).forEach(key => {
        el.setAttribute(key, props[key]);
    });

    (vnode.children || []).forEach(child => {
        el.appendChild(render(child));
    });

    return el;
}

The last missing piece is actual mounting. So let’s do it now.

const renderedExample = render(example);

document.querySelector('#app').appendChild(renderedExample);

Now we’re good to go. It’s time to test how the render function will handle our virtual node with the class prop.

It works like a charm!

screenshot.png

Live example on CodeSandbox.

It renders the div with correct class foo.

<div class="foo">Hello world!</div>

I added this simple bit of CSS to test if our class is in place. And it is, you can verify it yourself!

.foo {
    color: coral;
}

Now we are completely sure, that the reason behind className usage is not connected somehow to render function. We are sure because we implemented the render function, that uses class ourselves.
Now what? Maybe we should agree that it’s some kind of convention and leave things as they are? No, we should take an even closer look at the problem.

A different approach to the problem

You see, there is a JS framework, called Preact. It’s an alternative to React with the same API.
And there is a very interesting statement on its official page.

preact official page.png

Closer to the DOM. Hmm, it’s the exact thing, we are looking for. We try to use class, which is a native way of adding CSS classes in DOM. And Preact uses this approach, it becomes clear from its official docs.

Preact aims to closely match the DOM specification supported by all major browsers. When applying props to an element, Preact detects whether each prop should be set as a property or HTML attribute. This makes it possible to set complex properties on Custom Elements, but it also means you can use attribute names like class in JSX:


<div class="foo" />


<div className="foo" />

So, let’s dig into Preact source code to figure out why it works.

Explore source code

Here is a link to the source file on GitHub, in case you want to follow along.

Let’s take a look at Preact createElement function, which serves similar purpose as React.createElement. Here’s a snippet from the function body.

function createElement(type, props, children) {
    let normalizedProps = {},
            key,
            ref,
            i;
    for (i in props) {
        if (i == 'key') key = props[i];
        else if (i == 'ref') ref = props[i];
        else normalizedProps[i] = props[i];
    }
    

Preact createElement function filters out only two properties, key and ref, and passes others to normalizedProps.

Sidenote: If you’re asking yourself, why Preact filters out key and ref and how these special props are handled internally by Preact, let me know in the comments section. I’ll make detailed articles about these two props.

Then Preact passes the resulting normalizeProps to another function, called createVNode, and returns the result.

    
    return createVNode(type, normalizedProps, key, ref, null);
}

Let’s dig into createVNode function.

Source file on GitHub

function createVNode(type, props, key, ref, original) {
    const vnode = {
        type,
        
        props,
        
    };
    
    
    
    return vnode;
}

It becomes obvious from the snippet, that the createVNode function doesn’t do any transformations with passed props. It just returns the props in the new vnode object. And vnode object is just a representation of a given DOM element and it’ll be rendered to the real DOM in the future, as we now know.

So the question is, how does Preact know either it is a complex property or HTML attribute if it passes all properties directly to the vnode, that gets rendered in the end? For example, how does the event system work in this setup?
Maybe the answer lies in the render phase? Let’s give this guess a shot.

There is a function, called setProperty, which is responsible for setting a property value on a DOM node, as you may have gathered. This function is the main mechanism of setting properties to DOM nodes in Preact.

Source file on GitHub

function setProperty(dom, name, value, oldValue, isSvg) {
    
    else if (name[0] === 'o' && name[1] === 'n') {
        
        dom.addEventListener(name, handler)
    }
}

So Preact actually checks whether the property name corresponds to some event and adds an event listener if it’s the case.
Such distinction allows Preact to deal with events passed through onClick, onInput, and other props like these, but at the same time allows to use standard HTML properties, like class instead of unique-to-JSX className.
But how does Preact handle user-defined custom props? The answer lies in the question itself.

You see, we as a developers, may only pass custom properties to our own components. For example, let’s define custom UserDefinedComponent.


import { h } from 'preact';

const UserDefinedComponent = ({exampleFunc, brandText}) => {
    exampleFunc();

    return (
        <div>
            <p>{brandText}</p>
        </div>
    );
}

export default UserDefinedComponent;

And render it in the App component.


import { h } from 'preact';
import UserDefinedComponent from './UserDefinedComponent';

const App = () => {
    return (
        <UserDefinedComponent 
            exampleFunc={() => {
                console.log('Hello world!')
            }
            brandText="Hello world!"
        />
    )
}

As you may see, there is no way how exampleFunc and brandText would be passed to the real HTML elements. And even if you intentionally do this, the browser will just ignore unknown properties, Preact doesn’t need to additionally validate them on its side.

But why does React use camelCase property naming convention instead of HTML attribute names, anyway?

The last question

There is no clear answer to this question. We may only make a few guesses.

Maybe, it’s really just a convention, that was proposed when React wasn’t event public.

Or maybe, React developers want to match the JavaScript API more closely, than HTML one. Because in JS the standard way to access Element class property is Element.className.

const element = document.querySelector('.example');

const classList = element.className;
element.className = 'new-example';

It doesn’t really matter at this point why they’ve done so. What matters is, that we now understand all nitty-gritty details about it!

Wrap up

Today we learned

Let’s sum up what we learned today.

  • The reason why React uses the camelCase property is probably not one of these:
    • class is a reserved keyword in JavaScript
    • camelCase properties can’t be handled by JSX
    • camelCase properties mess up render function
  • Preact uses standard HTML properties, because:
    • It aims to closely match the DOM specification
    • It detects whether each prop should be set as a property or HTML attribute
  • Digging into source code is more fun, than frightening 😄

I’m looking forward to similar articles, what should I do?

First of all, if you really like this post leave a comment or/and a reaction to let me know, that I am going in the right direction. Any constructive feedback, either positive or negative, will be welcomed 🙏

If you want more content like this right now:

If you want more content like this next week:

  • Follow me on hashnode, I’ll try to do my best to post an article every week here or even make a special hashnode-exclusive series.
  • Follow me on dev.to, I am going to post an episode of the Deep-dive-into-React-codebase series this Sunday (January 16) at 6:00 am UTC+0.
  • Follow me on Twitter, if you want to know about every article I made and also read their sum-ups in threads.





Source link

Leave a reply

Please enter your comment!
Please enter your name here