A guest post from Fearless software engineer Michael Knoll. Michael is our resident in-house game developer. He created the Fearless Match Game as a test of player’s memory skills and a way to get to know new team members. His new game Lit-tac-toe uses lit.dev.
Like most developers I have come to rely on the component nature of react when building modern web applications. However, this comes at a steep cost. A react app created with “create-react-app” is going to score in the mid to upper seventies on a lighthouse benchmark. This means a load time of nearly 5 seconds before the page is interactive. That is a long time in today’s world of instant gratification. While it is possible to tune a react app to be faster, that is just one more problem I could do without.
lit.dev is a library that allows you to create native web components with all the reactivity you are used to from React and other similar frameworks. Inspired by this article that rewrites the react tutorial I set out to rewrite the React tic tac toe tutorial. This is React’s home turf so it should have an advantage.
To scaffold the project lit provides an analog to “create-react-app” in the form of
npm init @open-wc
which will allow you to create a well formatted project. I definitely want all of this built in:
A few more options and just like that I am able to serve a local page with nothing more than
npm run start
Allows me to serve a hot reloading page on http://localhost:8000. I can now begin the tutorial.
Lit preserves much of the syntactical sugar of React. Here is the creation of a native alert in response to a click of a button in React:
<button className=”square” onClick={function() { alert(‘click’); }}> {this.props.value}
</button>
And in Lit:
<button class=”square” @click=${() => alert(‘click’)}>
${this.value}
</button>
The React tutorial hand waves away the creation of some starter code, but we need to recreate that code and in the process learn a bit about how Lit works. The first step is to create and register a game element. To do that I created a file called “src/components/game.js” and filled it with the basic components of a lit component.
import { LitElement, html, css } from ‘lit-element’;export class Game extends LitElement {static get properties() {
return {
};
}static get styles() {
return css`
`;
} constructor() {
super();
} render() {
return html`
<div>
GAME IS HERE
</div> `;
}
}
In order to use a lit component it must be registered. We need to register the component as a custom element so I created a file called custom-elements.js. To that file I added these lines to register our component
import {Game} from ‘./components/game.js’ customElements.define(‘lit-tac-toe-game’, Game)
Finally, we need to put this code into the actual HTML page. To do this I replaced the body of index.html with
<lit-tac-toe-game></lit-tac-toe-game> <script type=”module” src=”./src/custom-elements.js”></script>
This serves to import all of the javascript we need into the project for now. You can check out this code from the github branch, or hopefully you have been writing as we go. If you still have the dev server running you can reload the page and see that everything is working. If it isn’t now is the time to double check your work until it is.
The React starter code defines 3 components, Game, Board and Square. Now that we have defined one component, we simply need to recreate the process for the remaining 2 components. Before we do that, it might be helpful to look into the basic parts of a lit component.
The properties section allows you to register the reactive parts of your component. If a variable is defined in the properties section any changes to it will be monitored and the page will be updated accordingly. Unlike React, there is no need to explicitly call setState, just assign a new value to your variable and Lit will figure out the rest for you.
The getStyles() method provides a way to deliver css directly to your component, and not the page at large. getStyles are applied only to the shadow dom and not the full dom tree. You can read more about it here, but in my opinion it is a very powerful tool to ensure consistent styling, and prevent integration issues with existing sites.
constructor and render should be relatively familiar to existing react users. They behave almost exactly as they do in React.The main difference is that without jsx you need to wrap any html you want to render in an html tag.
html`<div class=”myDiv” id=”firstDiv”>More code here</div>`
You no longer have to bastardize html element names like className and go back to writing idiomatic html, albeit with a little spice still.
Finally, calling code requires the addition of a $ in front of the curly braces. So to call the renderSquare function it looks like this:
${this.renderSquare(0)}
Having applied those changes we now have 4 files that have changed:
Custom-element.js:
import {Game} from ‘./components/game.js’
import {Board} from ‘./components/board.js’
import {Square} from ‘./components/square.js’customElements.define(‘lit-tac-toe-game’, Game)
customElements.define(‘lit-tac-toe-board’, Board)
customElements.define(‘lit-tac-toe-square’, Square)
components/square.js:
import { LitElement, html, css } from ‘lit-element’; export class Square extends LitElement { static get properties() {
return { };
} static get styles() {
return css`
.square {
background: #fff;
border: 1px solid #999;
float: left;
font-size: 24px;
font-weight: bold;
line-height: 34px;
height: 34px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
width: 34px;
} .square:focus {
outline: none;
}
`;
} constructor() {
super();
} render() {
return html`
<button class=”square”> </button>`;
}
}
components/board.js:
import { LitElement, html, css } from ‘lit-element’;export class Board extends LitElement {static get properties() {
return {};
}static get styles() {
return css`
.board-row:after {
clear: both;
content: “”;
display: table;
}
`;
}constructor() {
super();
}
renderSquare(i) {
return html`<lit-tac-toe-square></lit-tac-toe-square>`;
}
render() {const status = ‘Next player: X’;
return html`
<div>
<div class=”status”>${status}</div>
<div class=”board-row”>
${this.renderSquare(0)}
${this.renderSquare(1)}
${this.renderSquare(2)}
</div>
<div class=”board-row”>
${this.renderSquare(3)}
${this.renderSquare(4)}
${this.renderSquare(5)}
</div>
<div class=”board-row”>
${this.renderSquare(6)}
${this.renderSquare(7)}
${this.renderSquare(8)}
</div>
</div>`;
}
}
components/game.js:
import { LitElement, html, css } from ‘lit-element’;export class Game extends LitElement {static get properties() {
return {};
}static get styles() {
return css`
.game {
display: flex;
flex-direction: row;
}.game-info {
margin-left: 20px;
}`;
}constructor() {
super();
}render() {
return html`
<div class=”game”>
<div class=”game-board”>
<lit-tac-toe-board ></lit-tac-toe-board>
</div>
<div class=”game-info”>
<div>{/* status */}</div>
<ol>{/* TODO */}</ol>
</div>
</div>
`;
}
}
At this point we have effectively created the starter code provided for the react tutorial. Along the way we have learned how to:
- Initialize a new project
- Register a custom component
- Create a custom component
- Write html to render components
- Write css to style components
You can check out the starter code by cloning this branch and running it with npm run start
The first lesson in react is passing a property to a component via the props argument. Lit handles this slightly differently, in a slightly more declarative way. Instead of deconstructing or referencing the mystical props argument you declare your properties in the properties function and if that value is passed in then it is set. So we add this to the properties function in components/square.js and simply reference it in the render function.
components/square.js:
import { LitElement, html, css } from ‘lit-element’;export class Square extends LitElement {static get properties() {
return {
value: {type: Number}
};
}static get styles() {
return css`
.square {
background: #fff;
border: 1px solid #999;
float: left;
font-size: 24px;
font-weight: bold;
line-height: 34px;
height: 34px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
width: 34px;
}.square:focus {
outline: none;
}
`;
}constructor() {
super();
}render() {
return html`
<button class=”square”>
${this.value}
</button>`;
}
}
Now we just need to pass in the value via the board component. To do this the attribute name must begin with a . so we use “.value”. Change the renderSquare method to:
renderSquare(i) {
return html`<lit-tac-toe-square .value=${i}></lit-tac-toe-square>`;
}
We should now be caught up with this section of the React Tutorial and ready to proceed with making the component interactive.
Lit does not deal with state in the same way as react does so the lesson can simply conclude with this:
<button class=”square” @click=${() => this.value=’X’}>
${this.value}
</button>
At this point the React tutorial goes on for several paragraphs about immutability and why it is important. Lit inherently agrees with this position, although they do provide an escape hatch. If you have a property that is an array or an object you will need to assign the value a completely new object. This will be familiar as this pattern from react:
const newState = this.state.slice()
newState.someKey = someValue
this.setState(newState)
However, if for whatever reason you must mutate the object, you can request an update explicitly with
this.requestUpdate();
That will cause the component to re render. The lit docs are available here.
In React functions are passed down via props and then called from within child components in order to lift up state. To this end the React tutorial passes down an onClick method from board to squares. This syntax can be kind of confusing in my opinion. Lit provides an alternate method of handling the passing of information between parent and children. This involves the dispatch of a new custom event. To do this components/square.js has the following 2 functions:
handleClick(position){
this.dispatchEvent(new CustomEvent(“square-clicked”, { detail: position }));
}render() {
return html`
<button class=”square” @click=${() => this.handleClick(this.value)} >
${this.value}
</button>`;
}
When the square is clicked a new custom event is emitted. To listen to that event the parent component must implement a listener with the @<event> syntax so components/board.js has these functions:
handleClick(position){this.dispatchEvent(new CustomEvent(“board-clicked”, { detail: position }));
}renderSquare(i) {
return html`<lit-tac-toe-square .value=${this.squares[i]} @square-clicked=${() => this.handleClick(i)} ></lit-tac-toe-square>`;
}
@square-clicked triggers the execution of the handleClick function which is listened to by the game component. I find this 3 tiered structure more helpful in understanding how data flows between layers of the dom. In addition by only emitting events up one level it becomes trivial to follow the path of the data because each change requires a custom event. All together the library can be found in this branch. From here to the end of the tutorial there is no difference between Lit and React. They both use vanilla javascript to functionally, without any side effects, determine the winner. This is used to check if there is a winner and if so to end the game. I implemented the checks at different component levels just to illustrate how information flow might be controlled in a real application. The final product can be found on this branch. We have, with just a slight tweak to the syntax, recreated React’s best example for how to present itself to the world. This application has a lighthouse score of 89 when running off of localhost with no optimizations. By simply building it with the built in command and serving it via
dist$ python -m SimpleHTTPServer 8000
I get a perfect 100 lighthouse score for performance. I would recommend thinking twice before reaching for react on your next project and consider giving Lit a shot. It has everything you use in React, but with far, far less overhead.
There will be a part 2 where we finish up the tutorial and then begin to extend the game by adding real time game updates via websockets and a small number of python lambdas. Depending on how far that goes there might even be a part 3. The end product can be seen here