Skip to content

Instantly share code, notes, and snippets.

@MansourM61
Last active January 15, 2026 14:45
Show Gist options
  • Select an option

  • Save MansourM61/350031fe74808dd2f8f4793d80938f68 to your computer and use it in GitHub Desktop.

Select an option

Save MansourM61/350031fe74808dd2f8f4793d80938f68 to your computer and use it in GitHub Desktop.

Alpine.js Project Setup

Alpine.js is a lightweight framework to create reactive HTML pages.

Vanilla Code

Setup the Project

This guideline is for using Vanilla Alpine with Typescript. The alpinejs-component package does not come with TypeScript support. To solve this issues:

  1. The project can be created and written all in JavaScript,
  2. The project is created in TypeScript, but main.ts file is changed to main.js and "allowJs": true is added as compilerOptions in tsconfig.json file.
  3. The type for alpinejs-component package is generated manually using these steps. This option is preferred.

To use Alpine.js as the main framework:

  1. Install Vite as a Vanilla Typescript project:

    npm create vite@latest
  2. Install the packages:

    npm i alpinejs
    npm i -D @types/alpinejs
    npm i -D alpinejs-component
  3. Create typings.d.ts in the project root folder:

    import Alpine from "alpinejs";
    
    declare global {
      interface Window {
        Alpine: typeof Alpine;
      }
    }
  4. Include the typings in tsconfig.json file:

        "include": [
            "src",
            "./typings.d.ts"  // add this line
        ]
  5. 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
  6. 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
      }
    }
  7. Modify package.json file to include the compile command:

        "scripts": {
            "build": "node scripts/build.js",
            "compile": "npx tsc"
        },
  8. Compile the package by running:

    npm run compile
  9. Copy package.json and all generated files except for JavaScript files from dist to Vite project in node_modules/types/alpinejs-component folder.

  10. Change the main.ts file content to:

    import Alpine from "alpinejs";
    import component from "alpinejs-component";
    
    Alpine.plugin(component);
    window.Alpine = Alpine;
    Alpine.start();
  11. Restart TypeScript server in the editor.

Adding Component

  1. To add a new component, follow the steps from alpinejs-component documentation.
  2. Each component can also be broken down into TS/JS and HTML files.

The following files are just a simple demo:

index.html

<!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>

src/main.ts

import Alpine from "alpinejs";
import component from "alpinejs-component";
import "./components/comp";

window.Alpine = Alpine;

Alpine.plugin(component);

Alpine.start();

src/components/comp.html

<div x-data="comp">
  <button @click="toggle" x-text="title"></button>

  <div x-show="open" x-text="msg"></div>
</div>

src/components/comp.ts

import Alpine from "alpinejs";

Alpine.data("comp", () => ({
  open: false,

  toggle() {
    this.open = !this.open;
  },
}));

React Code

  1. Create a React Typescript using Vite:

    npm create vite@latest
  2. Install the packages:

    npm i alpinejs
    npm i -D @types/alpinejs
  3. Create typings.d.ts in the project root folder:

    import Alpine from "alpinejs";
    
    declare global {
      interface Window {
        Alpine: typeof Alpine;
      }
    }
  4. Include the typings in tsconfig.json file:

        "include": [
            "src",
            "./typings.d.ts"  // add this line
        ]
  5. Initialise Alpine.js in main.tsx by adding:

    import Alpine from "alpinejs";
    
    window.Alpine = Alpine;
    Alpine.start();

    at the module level.

  6. Add the Alpine.js HTML codes as a string variable to the React component.

  7. Use dangerouslySetInnerHTML attribute to render the string into HTML elements.

  8. Add the x-data variables using Alpine.data() function.

The following file are just a simple demo:

src/App.tsx

Single Alpine.js Components

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;

Multiple Alpine.js Components (without re-render)

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;

Multiple Alpine.js Components (with re-render)

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;

Alpine.js Code Separation

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.

src/App.tsx
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;
src/components/Comp.tsx
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 }} />;
}
src/components/comp.hook.ts
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;
      });
    },
  }));
}
src/components/comp.html.ts
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;
};
src/components/comp.html
<div x-data="comp(__DATA_KEY__)">
  <button @click="inc()" x-text="`__TITLE__: counter = ${ count }`"></button>
</div>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment