Telerik blogs

Learn how you can use Astro on its own to meet your SSR-first app needs.

Astro is a beautiful and straightforward JavaScript framework that allows you to build full-stack apps. While most people use Astro to plug and play components written for other frameworks, you can use a vanilla Astro file to meet your needs instead. Astro is made as an SSR or static framework first and for reactivity second.

Home - counter #1: 11, counter #2: 19, counter #3: 19. Counters #2 and #3 are shared!

TL;DR

Astro is its own framework, but it is an SSR framework with plugins for other framework components. If you need fine-grained reactivity, vanilla Astro probably isn’t for you. If you love writing pure JavaScript, vanilla Astro is for you. Vanilla Astro is for SSR-first applications that have very little reactivity.

Tailwind

First, Tailwind is easily accessible using npx.

npx astro add tailwind

Astro Files

Astro files end with the .astro syntax, allowing you to generate HTML quickly on the server. Components are reusable and composable.

Component Script

Astro uses code fence (- - -), similar to Markdown’s frontmatter, which allows you to work with JavaScript on the server. The goal is to write safe and secure JavaScript only available on the server.

---
// you can import files (as well as other frameworks)
import AstroComponent from '../components/astro-component.astro';

// import data
import data from '../src/data.json';

// fetch from API - (use external script for error handling)
const data = await fetch('/some-url/users').then(r => r.json());

// create variables and use props
const { title } = Astro.props;
---

<!-- HTML Template goes here -->

JSX

Components generate HTML code on the server using a JSX-similar syntax. This means you can use your variables inside { } and display them with ternaries, the map function and other JSX tricks.

<h2>{title ? 'Some Title' : 'Default Title'}</h2>

๐Ÿ“ Remember, this is not reactive and only generated from the backend.

Layouts and Slots

Like Svelte 4 and below, you can use slots and named slots to pass and stack components.

// layouts/main-layout.astro

---
const { title } = Astro.props;
---

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="viewport" content="width=device-width" />
    <meta name="generator" content={Astro.generator} />
    <title>{title}</title>
  </head>
  <body>
    <h1>{title}</h1>
    <slot />
  </body>
</html>

However, you need to make sure to use the layout manually.

---
import Counter1 from "../components/counter1.astro";
import Counter2 from "../components/counter2.astro";
import Counter3 from "../components/counter3.astro";
import MainLayout from "../layouts/main-layout.astro";
---

<MainLayout title="Home">
  <Counter1 />
  <Counter2 />
  <Counter3 />
</MainLayout>

๐Ÿ“ Layouts are usually stored in the layouts directory, although they are not significantly different from the slots.

Routing

Routing also works similarly to other frameworks with file-based routing by placing your .astro files in the pages directory.

# Example Routes
src/pages/index.astro        -> mysite.com/
src/pages/about.astro        -> mysite.com/about
src/pages/about/index.astro  -> mysite.com/about
src/pages/about/me.astro     -> mysite.com/about/me
src/pages/posts/1.md         -> mysite.com/posts/1

src/pages/posts/[name].astro     -> mysite.com/posts/name-of-post
src/pages/posts/[...tree].astro  -> mysite.com/posts/this/is/a/tree

๐Ÿ“ There are specific static methods using getStaticProps(), but in this post, I’m going to focus on using Astro as a dynamic framework.

Endpoints

You can also create dynamic endpoints like other SSR frameworks.

// src/pages/api/[id].json.ts

import type { APIRoute } from 'astro';

const usernames = ["Sarah", "Chris", "Yan", "Elian"]

export const GET: APIRoute = ({ params, request }) => {
  const id = params.id;
  return new Response(
    JSON.stringify({
      name: usernames[id]
    })
  )
}

Actions

Form Actions are relatively new in Astro, but they work as expected.

// some-page.astro
---
import { actions } from "astro:actions";
---

<form name="greetingForm" action={actions.greet} class="flex flex-col gap-3">
  <input
    class="..."
    name="name"
    placeholder="Your name..."
  />
  <Button color="orange" type="submit">Get greeting</Button>
</form>

The key is manually adding the HTML.

<script>
  import { actions } from "astro:actions";

  const form = document.forms.namedItem("greetingForm")!;

  form.onsubmit = async (event) => {
    event.preventDefault();
    const formData = new FormData(form);
    const { data, error } = await actions.greet(formData);
    if (!error) alert(data);
  };
</script>

Reactivity

Reactivity is only done on the client side. This means you must use script tags separate from the Astro Component Script.

Getting Items from the Document

Remember and practice pure JS techniques to avoid importing Vue, Svelte, React or other framework files.

// getting a form element directly returns type HTMLFormElement
const form = document.forms.namedItem('formName')!;

// get other items
const counterSpan = document.getElementById("counterSpan")!;

// notice the null assertions... we assume the items exist

Clickable Elements

One way to handle click events is to use onclick.

<Button onclick="increment()">Increment</Button>

However, you cannot call increment() directly in your script, you need to declare it on the window object.

window.increment = () => {
  // handle increment on click
};

This will also require you to add a global increment function to the window object in TS.

// env.d.ts

declare global {
    interface Window {
        increment: () => void
    }
}

export { };

๐Ÿ“ Remember to export something for the TS to be available everywhere in your app.

Even though you can get onclick to work in this fashion, it is not necessarily better than using an event handler.

// method 2

const counterButton = document.getElementById("counterButton")!;

counterButton.onclick = () => {
  // handle increment
};

// OR method 3

const increment = () => { 
  // handle increment
};

counterButton3.addEventListener("click", increment);

// removing onclick is not always necessary, but good practice

window.addEventListener("beforeunload", () => {

  counterButton.removeEventListener("click", increment);
  
  // OR
  
  counterButton.removeAttribute('onclick');
  
  // OR
  
  counterButton.onclick = null;  
  
});

Updating the DOM

While updating the DOM on the server is extremely easy, it requires more control in pure JS.

<span id="my-text">Some Text</span>

<script>
  const spanEl = document.getElementById('my-text')!;
  
  spanEL.textContent = 'Some New Text';  
</script>

Reactive Variables

Astro recommends you use nanostores, a small signal-based pack, to handle reactive variables that change.

npm i nanostores

Here is a simple counter.

export const useCounter = () => {
    
    const _counter = atom(0);

    return {
        increment: () => {
            _counter.set(_counter.get() + 1);
        },
        atomCounter: _counter
    };
};

Wrapping variables in functions like this is good practice, as you have better control over what is available.

<span id="counterSpan">0</span>

<script>
  import { useCounter } from "./use-counter";

  const counter = useCounter();
  
  const counterSpan = document.getElementById("counterSpan")!;

  window.onbeforeunload = counter.atomCounter.subscribe((count) => {
    counterSpan.textContent = count.toString();
  });
  
</script>

You can use the counter code as many times as you like by importing the useCounter function. You must subscribe to the changes to the atom and update the textContent string.

export const sharedCounter = useCounter();

For any shared state, you must export the variable directly in an outside .ts file.

When NOT to Use Vanilla Astro

You can always use a .astro file for everything, but the more complex your reactivity, the harder writing vanilla JavaScript can get. Frameworks like Svelte and SolidJS are optimized for these use cases and are compiled to update only the necessary changed items. For example, a list of todo items does not need to be updated every time there are changes. There should be fine-grained reactivity to appendChild, removeChild, etc., for only the changed todo items inside the changed HTML tags. While always possible with JS, the developer experience will be better with a Svelte or SolidJS component instead.

Best Usage

Astro is best used with a combination of all frameworks. If you want to use Astro’s wonderful SSR functionality and need very little reactivity, use vanilla Astro. Use a Svelte or SolidJS component if you need complex reactivity for some routes. If you need to use a custom package only available for React, you may need to use a React component for that route.

Astro is extremely versatile and can be used by all JavaScript developers. Vanilla Astro is for those who want to optimize.


About the Author

Jonathan Gamble

Jonathan Gamble has been an avid web programmer for more than 20 years. He has been building web applications as a hobby since he was 16 years old, and he received a post-bachelor’s in Computer Science from Oregon State. His real passions are language learning and playing rock piano, but he never gets away from coding. Read more from him at https://code.build/.

 

 

Related Posts

Comments

Comments are disabled in preview mode.