State
State is the internal reactive data owned and managed exclusively by the component. Unlike props, state is not exposed or mapped to HTML attributes, and it is intended to store data that changes over the lifecycle of the component (e.g., UI toggle status, input values, API fetch results).
Declaring Initial State
To define a component's internal state, assign an object to the initialState class property. Each key in this object will be initialized as a reactive state getter.
1import { WebComponent } from '@beforesemicolon/web-component'2 3class CounterElement extends WebComponent {4 initialState = {5 count: 0,6 label: 'Clicks',7 }8}Reading State in Templates
Like props, the properties of this.state are reactive getter functions. You can access the values inside your templates by passing the getter directly or by invoking it.
- Pass the getter directly to let the template handle reactive bindings:
this.state.count - Invoke it as a function when executing calculations or inside conditional blocks:
this.state.count()
1import { WebComponent, html } from '@beforesemicolon/web-component'2 3class CounterElement extends WebComponent {4 initialState = {5 count: 0,6 label: 'Clicks',7 }8 9 render() {10 return html`11 <div>12 <span>${this.state.label}: ${this.state.count}</span>13 </div>14 `15 }16}Modifying State via setState
To update state reactively, call this.setState(). This method accepts either a partial state object or a callback function.
Object Merge Format
You can pass a partial object containing the fields you want to update. WebComponent will merge the updates into the state automatically, so you do not need to manually spread the rest of the state.
1// Updates only the count; label remains untouched2this.setState({ count: 10 })Callback Format
If your new state depends on the previous state, pass a callback function. The callback receives the current state object with its evaluated values (not getters) and must return a partial state object.
1this.setState((prev) => ({2 count: prev.count + 1,3}))Lifecycle Restrictions
State updates trigger DOM updates. Because of this, calling this.setState() before the component is mounted (i.e. when this.mounted is false) or after it has been unmounted will result in an error.
1class MyComponent extends WebComponent {2 initialState = { data: null }3 4 constructor() {5 super()6 // ERROR: Cannot update state while component is unmounted.7 this.setState({ data: 'foo' })8 }9}If you need to fetch data or trigger state updates as soon as the component loads, do so in the onMount lifecycle hook:
1class MyComponent extends WebComponent {2 initialState = { data: null }3 4 onMount() {5 fetch('/api/data')6 .then((res) => res.json())7 .then((data) => {8 if (!this.mounted) return9 this.setState({ data })10 })11 }12}Derived UI Example
Keep the source state intact and derive display values inside functions.
1import { WebComponent, html, repeat } from '@beforesemicolon/web-component'2 3class SearchableList extends WebComponent {4 initialState = {5 query: '',6 items: ['Alpha', 'Beta', 'Gamma'],7 }8 9 results = () => {10 const query = this.state.query().toLowerCase()11 return this.state.items().filter((item) => {12 return item.toLowerCase().includes(query)13 })14 }15 16 render() {17 return html`18 <input19 value="${this.state.query}"20 oninput="${(event) =>21 this.setState({ query: event.target.value })}"22 />23 <ul>24 ${repeat(25 this.results,26 (item) => html`<li>${item}</li>`,27 () => html`<li>No matches.</li>`28 )}29 </ul>30 `31 }32}