Alpine.js is a lightweight framework to create reactive HTML pages.
This guideline is for using Vanilla Alpine with Typescript. The alpinejs-component package does not come with TypeScript support. To solve this issues:
- The project can be created and written all in JavaScript,
- The project is created in TypeScript, but main.ts file is changed to main.js and
"allowJs": trueis added ascompilerOptionsin tsconfig.json file. - The type for alpinejs-component package is generated manually using these steps. This option is preferred.
To use Alpine.js as the main framework:
-
Install Vite as a Vanilla Typescript project:
npm create vite@latest
-
Install the packages:
npm i alpinejs npm i -D @types/alpinejs npm i -D alpinejs-component
-
Create typings.d.ts in the project root folder:
import Alpine from "alpinejs"; declare global { interface Window { Alpine: typeof Alpine; } }
-
Include the typings in tsconfig.json file:
"include": [ "src", "./typings.d.ts" // add this line ]
-
Open the node_modules/alpinejs-component folder and install the following packages:
npm i -D typescript npm i -D @types/alpinejs npm i -D @types/node
-
Add tsconfig.json file to the package root folder with this content:
{ "include": ["src/**/*"], "compilerOptions": { "allowJs": true, "declaration": true, "emitDeclarationOnly": true, "outDir": "dist", "declarationMap": true } } -
Modify package.json file to include the compile command:
"scripts": { "build": "node scripts/build.js", "compile": "npx tsc" },
-
Compile the package by running:
npm run compile
-
Copy package.json and all generated files except for JavaScript files from dist to Vite project in node_modules/types/alpinejs-component folder.
-
Change the main.ts file content to:
import Alpine from "alpinejs"; import component from "alpinejs-component"; Alpine.plugin(component); window.Alpine = Alpine; Alpine.start();
-
Restart TypeScript server in the editor.
- To add a new component, follow the steps from alpinejs-component documentation.
- Each component can also be broken down into TS/JS and HTML files.
The following files are just a simple demo:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>vite-project</title>
<script defer type="module" src="/src/main.ts"></script>
</head>
<body>
<div x-data="{title: 'test'}">
<x-component
url="/src/components/comp.html"
x-data="{ title: title, msg: 'Content Visible!' }"
></x-component>
</div>
</body>
</html>import Alpine from "alpinejs";
import component from "alpinejs-component";
import "./components/comp";
window.Alpine = Alpine;
Alpine.plugin(component);
Alpine.start();<div x-data="comp">
<button @click="toggle" x-text="title"></button>
<div x-show="open" x-text="msg"></div>
</div>import Alpine from "alpinejs";
Alpine.data("comp", () => ({
open: false,
toggle() {
this.open = !this.open;
},
}));-
Create a React Typescript using Vite:
npm create vite@latest
-
Install the packages:
npm i alpinejs npm i -D @types/alpinejs
-
Create typings.d.ts in the project root folder:
import Alpine from "alpinejs"; declare global { interface Window { Alpine: typeof Alpine; } }
-
Include the typings in tsconfig.json file:
"include": [ "src", "./typings.d.ts" // add this line ]
-
Initialise Alpine.js in
main.tsxby adding:import Alpine from "alpinejs"; window.Alpine = Alpine; Alpine.start();
at the module level.
-
Add the Alpine.js HTML codes as a string variable to the React component.
-
Use
dangerouslySetInnerHTMLattribute to render the string into HTML elements. -
Add the
x-datavariables usingAlpine.data()function.
The following file are just a simple demo:
import { useEffect, useState } from "react";
import "./App.css";
import Alpine from "alpinejs";
const alpineTemplate = (title: string) =>
`
<div x-data="comp">
<button @click="inc()" x-text="` +
`\`${title}` +
`: counter = \${count}\`"></button>
</div>`;
const AlpineWidget = ({ title }: { title: string }) => {
const htmlElement = alpineTemplate(title);
return <div dangerouslySetInnerHTML={{ __html: htmlElement }} />;
};
function App() {
const [reactCount, setReactCount] = useState(0);
const [aplinejsCount, setAplinejsCount] = useState(0);
Alpine.data("comp", () => ({
count: aplinejsCount,
inc() {
this.count += 1;
setAplinejsCount(this.count);
},
}));
useEffect(() => {
console.log("React: count =", reactCount);
console.log("Alpine.js: count =", aplinejsCount);
}, [reactCount, aplinejsCount]);
return (
<>
<button
onClick={() => setReactCount((c) => c + 1)}
>{`React: Counter = ${reactCount}`}</button>
<AlpineWidget title="Alpine.js" />
</>
);
}
export default App;In case of creating multiple version of the same Alpine.js component, use a dataKey to distinguish different versions. Using useState and directly updating the state variable, it is possible to suppress Alpine.js re-render step. Therefore, Alpine.js will internally update its variables and UI, but React will not re-render.
import { useEffect, useState } from "react";
import "./App.css";
import Alpine from "alpinejs";
const alpineTemplate = (title: string, dataKey: number) =>
`
<div x-data="comp(` +
`${dataKey}` +
`)">
<button @click="inc()" x-text="` +
`\`${title}` +
`: counter = \${count}\`"></button>
</div>`;
const AlpineWidget = ({
title,
dataKey,
}: {
title: string;
dataKey: number;
}) => {
const htmlElement = alpineTemplate(title, dataKey);
return <div dangerouslySetInnerHTML={{ __html: htmlElement }} />;
};
function App() {
console.log("App re-rendered");
const [reactCount, setReactCount] = useState(0);
const [alpineJSCount, setAlpineJSCount] = useState([0, 0]);
Alpine.data("comp", (dataKey: number) => ({
count: alpineJSCount[dataKey],
inc() {
this.count += 1;
setAlpineJSCount((c) => {
c[dataKey] = this.count;
return c;
});
},
}));
useEffect(() => {
console.log("React: count =", reactCount);
console.log("Alpine.js: count =", alpineJSCount);
}, [reactCount, alpineJSCount]);
return (
<>
<button
onClick={() => setReactCount((c) => c + 1)}
>{`React: Counter = ${reactCount}`}</button>
<AlpineWidget title="Alpine.js 1" dataKey={0} />
<AlpineWidget title="Alpine.js 2" dataKey={1} />
</>
);
}
export default App;If re-render is needed, the state must be re-created or simply other state manager such as Immer is used.
import { useEffect, useState } from "react";
import "./App.css";
import Alpine from "alpinejs";
import { useImmer } from "use-immer";
const alpineTemplate = (title: string, dataKey: number) =>
`
<div x-data="comp(` +
`${dataKey}` +
`)">
<button @click="inc()" x-text="` +
`\`${title}` +
`: counter = \${count}\`"></button>
</div>`;
const AlpineWidget = ({
title,
dataKey,
}: {
title: string;
dataKey: number;
}) => {
const htmlElement = alpineTemplate(title, dataKey);
return <div dangerouslySetInnerHTML={{ __html: htmlElement }} />;
};
function App() {
const [reactCount, setReactCount] = useState(0);
const [alpineJSCount, setAlpineJSCount] = useImmer([0, 0]);
Alpine.data("comp", (dataKey: number) => ({
count: alpineJSCount[dataKey],
inc() {
this.count += 1;
setAlpineJSCount((c) => {
c[dataKey] = this.count;
return c;
});
},
}));
useEffect(() => {
console.log("React: count =", reactCount);
console.log("Alpine.js: count =", alpineJSCount);
}, [reactCount, alpineJSCount]);
return (
<>
<button
onClick={() => setReactCount((c) => c + 1)}
>{`React: Counter = ${reactCount}`}</button>
<AlpineWidget title="Alpine.js 1" dataKey={0} />
<AlpineWidget title="Alpine.js 2" dataKey={1} />
</>
);
}
export default App;The ultimate solution to create a project with React and Alpine.js, where Alpine.js codes are organised into separated codes is done though the following pattern.
import { useEffect, useState } from "react";
import "./App.css";
import { useImmer } from "use-immer";
import { useAlpine } from "./components/comp.hook";
import AlpineWidget from "./components/comp";
function App() {
const [reactCount, setReactCount] = useState(0);
const [alpineJSCount, setAlpineJSCount] = useImmer([0, 0]);
useAlpine(alpineJSCount, setAlpineJSCount);
useEffect(() => {
console.log("React: count =", reactCount);
console.log("Alpine.js: count =", alpineJSCount);
}, [reactCount, alpineJSCount]);
return (
<>
<button
onClick={() => setReactCount((c) => c + 1)}
>{`React: Counter = ${reactCount}`}</button>
<AlpineWidget title="Alpine.js 1" dataKey={0} />
<AlpineWidget title="Alpine.js 2" dataKey={1} />
</>
);
}
export default App;import type { JSX } from "react";
import { alpineTemplate } from "./comp.html";
export default function AlpineWidget({
title,
dataKey,
}: {
title: string;
dataKey: number;
}): JSX.Element {
const htmlElement = alpineTemplate(title, dataKey);
return <div dangerouslySetInnerHTML={{ __html: htmlElement }} />;
}import Alpine from "alpinejs";
import type { Updater } from "use-immer";
export function useAlpine(
alpineJSCount: number[],
setAlpineJSCount: Updater<number[]>
) {
Alpine.data("comp", (dataKey: number) => ({
count: alpineJSCount[dataKey],
inc() {
this.count += 1;
setAlpineJSCount((c) => {
c[dataKey] = this.count;
return c;
});
},
}));
}import template from "./comp.html?raw";
export const alpineTemplate = (title: string, dataKey: number): string => {
const formattedStr = template
.replace("__DATA_KEY__", `${dataKey}`)
.replace("__TITLE__", `${title}`);
return formattedStr;
};<div x-data="comp(__DATA_KEY__)">
<button @click="inc()" x-text="`__TITLE__: counter = ${ count }`"></button>
</div>