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 = /*#__PURE__*/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 = /*#__PURE__*/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 = { // defines the type of a given vnode type: "div", // defines all passed React props and HTML attributes of a given vnode props: { class: "foo" }, // contains children of a given vnode 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 elements 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!
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.
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 likeclass
in JSX:
// This:
<div class="foo" /> // ...is the same as:
<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.
function createVNode(type, props, key, ref, original) { const vnode = { type, // No props transformation here props, // ... }; // ... // No props transformation here either // ... 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.
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
.
// UserDefinedComponent.js
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.
// App.js
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 JavaScriptcamelCase
properties can’t be handled by JSXcamelCase
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
-
Follow me on Twitter, if you want to know about every article I made and also read their sum-ups in threads.
Why you have to use className in React, but not in Preact?
Source: Super Trending News PH
Post a Comment
0 Comments