Handle Page State in Astro
This guide won’t cover persistent state management across page navigations. For that, you’ll want to take a look at nanostores. This guide focuses solely on state management within a page
Astro brings the wonderful world of components to static sites. But with components comes the need to share and manage state. Using a Proxy
, you can create a simple state store that can be imported into any script in the project.
Creating a State Store
Start by creating a new file called state.js
in the src
directory. This file will contain the state store. As an example, we’ll use count
as a state property.
export const myState = new Proxy( { count: 0, listeners: [], listen: (fn) => myState.listeners.push(fn), }, { set: (target, key, value) => { if (key === 'listen' || key === 'listeners') return false target[key] = value target.listeners.map(fn => fn()) return true }, })
The state
object is a Proxy
that will intercept any changes to the object in set
. listen
allows you to add event listeners to the state store. Useful for updating the UI when the state changes.
Using the State Store
Once you’ve defined a state store, you can import it in the script
tag of any .astro
file in your project. In addition, I believe this works in any UI framework file too, as long as you specify client:only
, though I haven’t tested this, as each framework has its own state management solutions.
As an example, in src/pages/index.astro
create a script tag and import the state store.
---// server magic---
<p>Count: <span id="count">0</span></p><button id="increment">Increment</button>
<script> import { myState } from '../state.js'
// listen for changes to the state myState.listen(() => { console.log(myState.count) document.getElementById('count').innerText = myState.count })
// increment the count when the button is clicked document.getElementById('increment').addEventListener('click', () => { myState.count++ })</script>
We can also import the state store in other .astro
components and use it there, and state will remain consistent across all components. For example, editing the state in src/components/counter.astro
would update the state in src/pages/index.astro
.
Typing the State Store
If you’re using TypeScript, you can type the state store and it’s values to make it easier to work with.
export interface State { count: number listeners: Function[] listen: (fn: Function) => void}
export const myState: State = new Proxy<State>( { count: 0, listeners: [], listen: (fn) => myState.listeners.push(fn), }, { set: (target, key: keyof State, value) => { if (key === 'listen' || key === 'listeners') return false target[key] = value target.listeners.map(fn => fn()) return true }, })
Use-cases
My original use-case for this was to manage the state between my navigation & sidebar components on mobile. As the button to open the sidebar was on the navigation menu, I needed a way to open the sidebar from the navigation component. Using a Proxy
like this, I’m able to listen for changes to the state of the sidebar and update the UI accordingly.
You can use this to manage the state of a modal, form, or any other UI state that has a need to exist over multiple components. Nanostores is great for managing state across page navigations, but if you need to manage state within a page, it’s a little overkill.