Categories:

Lit vs. React: A comparison guide


Selecting a frontend framework can be a difficult decision for a developer because there are so many options. React is one of the most popular choices. It is well established and has an 84% satisfaction rating as of the 2021 State of JS Survey. Still, there are several other frameworks with interesting features and functionality that are worth investigating.

When selecting a frontend framework for your next project, consider the following questions:

  • Does this framework have the features I require?
  • How fast is this framework compared to others?
  • How easy is this framework to learn and use?
  • What size community uses this framework?

One alternative to React is Lit, which has a 77% satisfaction rating as of the 2021 State of JS Survey. Lit is easy to learn and use and its small footprint translates to fast loading times.

In this tutorial, we’ll compare React and Lit. We’ll also create a sample project in Lit.

Jump ahead:

  • What’s new in Lit?
  • Lit vs. React
  • JSX and templating
  • Components and props
  • State and lifecycle methods
  • Hooks
  • Refs
  • Creating a basic to-do project in Lit
  • Should I switch from React to Lit?

Let’s get started!

What’s new in Lit?

Lit has several features that distinguish it from other frontend frameworks:

  • LitElement base class is the convenient and versatile extension of the native HTMLElement. This class can be extended to define our components
  • Expressive and declarative templates make it easy to define how a component should be rendered
  • Reactive properties are the internal state of Lit’s components. Components automatically re-render when a reactive property changes
  • Scoped styles help keep our CSS selectors simple, ensuring our component styles do not affect other contexts
  • Supports Vanilla Javascript, TypeScript, and ergonomics (decorators and type declarations)

Lit vs. React

Lit’s core concepts and features are similar to those of React in many ways, but there are some significant differences. For example, React has been around since 2013, and is far more popular than Lit. At the time of this writing, React has around 15.9 million weekly downloads on npm compared to 127k weekly downloads on npm for Lit.

However, Lit is faster than React and also takes up less memory. A public benchmark comparison showed lit-html to be 8-10 percent faster than React’s VDOM. Lit has a minified memory size of 5kB, compared to 40kB for React.

These two frameworks offer other cool features, as well. Let’s see how they compare.

JSX and templating

JSX is a syntax extension to JavaScript that functions similarly to a templating language, but with the full power of JavaScript. React users can use JSX to easily write templates in JavaScript code. Lit templates serve a similar purpose, but express a component UI as a function of their state.

Here’s an example of JSX templating in React:

import 'react';
import ReactDOM from 'react-dom';

const name = 'World';
const el = (
  <>
    <h1>Hello, {name}</h1>
    <div>How are you? </div>
  </>
);
ReactDOM.render(
  el,
  mountNode
);

Here’s an example of templating in Lit:

import {html, render} from 'lit';

const name = 'World';
const el = html`
  <h1>Hello, ${name}</h1>
  <div>How are you?</div>`;

render(
  el,
  mountNode
);

As we can see in the above examples, Lit does not need a React fragment to group multiple elements in its templates. instead, Lit templates are wrapped with an HTML tagged template literal.

Components and props

Components are self-contained, reusable pieces of code. They perform the same action as JavaScript functions, but they work independently and return HTML. React components are classified into two types: class components and functional components.

Class components

The Lit equivalent of a React class component is called LitElement.

Here’s an example of a class-based component in React:

import React from 'react';
import ReactDOM from 'react-dom';

class Welcome extends React.Component {
  constructor(props) {
    super(props);
    this.state = {name: ''};
  }

  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

const el = <Welcome name="World"/>
ReactDOM.render(
  el,
  mountNode
);

Here’s the same example in Lit, using LitElement:

import {LitElement, html} from 'lit';

class WelcomeBanner extends LitElement {
  static get properties() {
    return {
      name: {type: String}
    }
  }

  constructor() {
    super();
    this.name = '';
  }

  render() {
    return html`<h1>Hello, ${this.name}</h1>`
  }
}

customElements.define('welcome-banner', WelcomeBanner);

After defining and rendering the template for the LitElement component, we add the following to our HTML file:

<!-- index.html -->
<head>
  <script type="module" src="./index.js"></script>
</head>
<body>
  <welcome-banner name="World"></welcome-banner>
</body>

Now, let’s look at how functional components are created in these frameworks.

Functional components

Lit does not use JSX, so there’s no one-to-one correlation to a React functional component. However, it is simpler to write a function that takes in properties and then renders DOM based on those properties.

Here’s an example of a functional component in React:

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

const el = <Welcome name="World"/>
ReactDOM.render(
  el,
  mountNode
);

Here’s the same example in Lit:

import {html, render} from 'lit';

function Welcome(props) {
  return html`<h1>Hello, ${props.name}</h1>`;
}

render(
  Welcome({name: 'World}),
  document.body.querySelector('#root')
);

State and lifecycle methods

state is a React object that contains data or information about the component. The state of a component can change over time. Whenever its state changes, the component re-renders.

Lit’s reactive properties is a mix of React’s state and props. When changed, reactive properties can trigger the component lifecycle, re-rendering the component and optionally being read or written to attributes. Reactive properties come in two variants:

  • Public reactive properties
  • Internal reactive state

Reactive properties are implemented in React, like so:

import React from 'react';

class MyEl extends React.Component {
  constructor(props) {
    super(props)
    this.state = {name: 'there'}
  }

  componentWillReceiveProps(nextProps) {
    if (this.props.name !== nextProps.name) {
      this.setState({name: nextProps.name})
    }
  }
}

Reactive proeprtiers are implemented in Lit, like so:

import {LitElement} from 'lit';
import {property} from 'lit/decorators.js';

class MyEl extends LitElement {
  @property() name = 'there';
}

Internal reactive state refers to reactive properties that are not exposed to the component’s public API. These state properties lack corresponding attributes and are not intended to be used outside of the component. The internal reactive state of the component should be determined by the component itself.

React and Lit have a similar lifecycle, with some small but notable differences. Let’s take a closer look at some of the methods that these frameworks have in common.

constructor

The constructor method is available in both React and Lit. It is automatically called when an object is created from a class.

Here’s an example of the constructor method in React:

import React from 'react';
import Chart from 'chart.js';

class MyEl extends React.Component {
  constructor(props) {
    super(props);
    this.state = { counter: 0 };
    this._privateProp = 'private';
  }

Here’s an example of the constructor method in Lit:

class MyEl extends LitElement {
  static get properties() {
    return { counter: {type: Number} }
  }
  constructor() {
    this.counter = 0;
    this._privateProp = 'private';
  }

render

The render method is available in both React and Lit. It displays the code inside the specified element.

Here’s an example of the render method in React:

render() {
    return <div>Hello World</div>
  }

Here’s an example of the render method in Lit:

render() {
    return html`<div>Hello World</div>`;
  }

componentDidMount vs. firstUpdated and connectedCallback

The componentDidMount function in React is similar to a combination of Lit’s firstUpdated and connectedCallback lifecycle callbacks. This function is invoked after a component is mounted.

Here’s an example of the componentDidMount method in React:

componentDidMount() {
    this._chart = new Chart(this.chartElRef.current, {...});
  }

  componentDidMount() {
    this.window.addEventListener('resize', this.boundOnResize);
  }

Here’s an example of the firstUpdated and connectedCallback lifecycle callbacks in Lit:

firstUpdated() {
    this._chart = new Chart(this.chartEl, {...});
  }

  connectedCallback() {
    super.connectedCallback();
    this.window.addEventListener('resize', this.boundOnResize);
  }

componentDidUpdate vs. updated

The componentDidUpdate function in React is equivalent to updated in Lit. It is invoked after a change to the component’s props or state.

Here’s an example of the componentDidUpdate method in React:

componentDidUpdate(prevProps) {
    if (this.props.title !== prevProps.title) {
      this._chart.setTitle(this.props.title);
    }
  }

Here’s an example of the updated method in Lit:

updated(prevProps: PropertyValues<this>) {
    if (prevProps.has('title')) {
      this._chart.setTitle(this.title);
    }
  }

componentWillUnmount vs.disconnectedCallback

The componentWillUnmount function in React is equivalent to disconnectedCallback in Lit. This function is invoked after a component is destroyed or is unmounted.

Here’s an example of the componentWillUnmount method in React:

componentWillUnmount() {
    this.window.removeEventListener('resize', this.boundOnResize);
  }
}

Here’s an example of the disconnectedCallback method in Lit:

disconnectedCallback() {
    super.disconnectedCallback();
    this.window.removeEventListener('resize', this.boundOnResize);
  }
}

Hooks

Hooks are functions that allow React functional components to “hook into” React state and lifecycle features. Hooks do not work within classes, but they allow us to use React without classes.

Unlike React, Lit does not offer a way to create custom elements from a function, but LitElement does address most of the main issues with React class components by:

  • Not taking arguments in the constructor
  • Auto-binding all @event bindings (generally, to the custom element’s reference)
  • Instantiating class properties as class members

Here’s an example of Hooks in React (at the time of making hooks):

 
import React from 'react';
import ReactDOM from 'react-dom';

class MyEl extends React.Component {
  constructor(props) {
    super(props); // Leaky implementation
    this.state = {count: 0};
    this._chart = null; // Deemed messy
  }

  render() {
    return (
      <>
        <div>Num times clicked {count}</div>
        <button onClick={this.clickCallback}>click me</button>
      </>
    );
  }

  clickCallback() {
    // Errors because `this` no longer refers to the component
    this.setState({count: this.count + 1});
  }
}

Here’s the same example, using LitElement:

class MyEl extends LitElement {
  @property({type: Number}) count = 0; // No need for constructor to set state
  private _chart = null; // Public class fields introduced to JS in 2019

  render() {
    return html`
        <div>Num times clicked ${count}</div>
        <button @click=${this.clickCallback}>click me</button>`;
  }

  private clickCallback() {
    // No error because `this` refers to component
    this.count++;
  }
}

Refs

Refs are React functions that allow us to access the DOM element and any React elements that we’ve created. They are used when we want to change the value of a child component without using props.

In Lit, refs are created using the @query and @queryAll decorators. These decorators are nearly equivalent to querySelector and querySelectorAll, respectively, and render directly to the DOM.

Here’s an example of the refs function in React:

const RefsExample = (props) => {
 const inputRef = React.useRef(null);
 const onButtonClick = React.useCallback(() => {
   inputRef.current?.focus();
 }, [inputRef]);

 return (
   <div>
     <input type={"text"} ref={inputRef} />
     <br />
     <button onClick={onButtonClick}>
       Click to focus on the input above!
     </button>
   </div>
 );
};

Here’s the same example in Lit using the @query decorator:

@customElement("my-element")
export class MyElement extends LitElement {
  @query('input') // Define the query
  inputEl!: HTMLInputElement; // Declare the prop

  // Declare the click event listener
  onButtonClick() {
    // Use the query to focus
    this.inputEl.focus();
  }

  render() {
    return html`
      <input type="text">
      <br />
      <!-- Bind the click listener -->
      <button @click=${this.onButtonClick}>
        Click to focus on the input above!
      </button>
   `;
  }
}

Creating a basic to-do project in Lit

Let’s take a look at Lit in action by creating a sample to-do project.

To get started, run the command to clone the Lit starter JavaScript project:

git clone https://github.com/lit/lit-element-starter-js.git

Then, cd to the project folder and install the required packages using this command:

npm install

When the installation is complete, proceed to the lit-element-starter-js/my-element.js file. Delete the boilerplates codes and create a Todo component with the following code snippet:

import {LitElement, html, css} from 'lit';
class Todo extends LitElement {
  constructor() {
    super();
  }
  render() {
    return html`
      <div class="todos-wrapper">
        <h4>My Todos List</h4>
        <input placeholder="Add task..."/>
        <button>Add</button>
        <div class="list">
            #Todo List
        </div>
      </div>
    `;
  }
}
customElements.define('my-element', Todo);

The above code creates a Todo component with a constructor method, where all reactive properties of the application will be defined, and a render method, which renders JSX containing an input field and button.

Next, let’s define the properties of the application. Since this is a to-do application, we’ll need a TodosList to store the tasks and an input property to get user input.

Now, we’ll add the below code snippet to the Todos class:

static properties = {
    TodosList: {type: Array},
    input: {type: String},
  };

Then, we’ll use the below code to assign initial values to the TodosList and input properties in the constructor method:

 this.TodosList = [];
 this.input = null;

Next, we’ll create a method to add and update a to-do task:

setInput(event) {
    this.input = event.target.value;
  }

  addTodo() {
      this.TodosList.push({
      name: this.input,
      id: this.TodosList.length + 1,
      completed: false,
    });
    this.requestUpdate();
  }

 updateTodo(todo) {
    todo.completed = !todo.completed;
    this.requestUpdate();
  }

We can see in the above code that the requestUpdate() function was called in the addTodo and updateTodo methods after modifying the state. These methods were mutating the TodosList property, so we called the requestUpdate() function to update the component state.

Next, we’ll modify the render method, to add event listeners to the methods created above and to display the to-do tasks.

 render() {
    return html`
      <div class="todos-wrapper">
        <h4>My Todos List</h4>
        <input placeholder="Add task..." @input=${this.setInput} />
        <button @click=${this.addTodo}>Add</button>
        <div class="list">
          ${this.TodosList.map(
            (todo) => html`
              <li
                @click=${() => this.updateTodo(todo)}
                class=${todo.completed && 'completed'}
              >
                ${todo.name}
              </li>
            `
          )}
        </div>
      </div>
    `;
  }

Finally, let’s add some styling to make the application look more appealing:

static styles = css`
    .todos-wrapper {
      width: 35%;
      margin: 0px auto;
      background-color: rgb(236, 239, 241);
      padding: 20px;
    }
    .list {
      margin-top: 9px;
    }
    .list li {
      background-color: white;
      list-style: none;
      padding: 6px;
      margin-top: 3px;
    }
    .completed {
      text-decoration-line: line-through;
      color: #777;
    }
    input {
      padding: 5px;
      width: 70%;
    }
    button {
      padding: 5px;
    }
  `;

Now, let’s run the application:

npm run serve

Here’s our sample to-do project!

Sample Project Todos List

Should I switch from React to Lit?

Every framework has unique strengths and weaknesses. React powers the web applications of many large companies, such as Facebook, Twitter, and Airbnb. It also has an extensive community of developers and contributors.

If you are currently using React and are happy with that choice, then I see no reason for you to switch. However, if you are working on a project that requires really fast performance, then you might consider using Lit.

To learn more about Lit, see its official documentation.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket. 

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — start monitoring for free.

Source: logrocket


Leave a Reply

Your email address will not be published. Required fields are marked *