Stateful Components

Stateful components are classes that extend nullstack and are able to hold state that reflects in the ui.

A productive full stack web framework should not force you to think about framework details.

Nullstack takes control of its subclasses and generates a proxy for each instance.

When you call anything on your class you are actually telling Nullstack what to do with the environment behind the scenes.

This allows you to use vanilla JavaScript operations like assigning to a variable and see the reflection in the dom.

Mutability

You can mutate instance variables to update your application state.

Functions are automatically bound to the instance proxy and can be passed as a reference to events.

Events are declared like normal HTML attributes.

import Nullstack from 'nullstack';

class Counter extends Nullstack {

  count = 0;

  increment() {
    this.count++;
  }
  
  render() {
    return (
      <button onclick={this.increment}> 
        {this.count}
      </button>
    )
  }

}

export default Counter;

💡 Updates are made in batches, usually while awaiting async calls, so making multiple assignments have no performance costs!

Events array

You can pass an array of events as prop and they will run in parallel

You can also pass falsy values to skip events conditionaly.

import Nullstack from 'nullstack';

class Counter extends Nullstack {

  count = 0;

  increment() {
    this.count++;
  }

  log() {
    console.log(this.count);
  }

  logUnlessZero() {
    console.log(this.count > 0);
  }
  
  render() {
    return (
      <button onclick={[this.increment, this.log, this.count > 0 && this.logUnlessZero]}> 
        {this.count}
      </button>
    )
  }

}

export default Counter;

Object Events

You can shortcut events that are simple assignments by passing an object to the event.

Each key of the object will be assigned to the instance.

import Nullstack from 'nullstack';

class Counter extends Nullstack {

  count = 0;
  
  render() {
    return (
      <button onclick={{count: this.count + 1}}> 
        {this.count}
      </button>
    )
  }

}

export default Counter;

Event Source

By default, events refer to this when you pass an object.

You can use the source attribute to define which object will receive the assignments.

import Nullstack from 'nullstack';

class Paginator extends Nullstack {
  
  render({params}) {
    return (
      <button source={params} onclick={{page: 1}}> 
        First Page
      </button>
    )
  }

}

export default Paginator;

✨ Learn more about context params.

💡 If you do not declare a source to the event, Nullstack will inject a source={this} at transpile time in order to completely skip the runtime lookup process!

Event Context

The context of the event is a spread of the Nullstack client context, the component context, and the props of the element that generated the event

import Nullstack, { NullstackClientContext } from 'nullstack';

interface CounterProps {
  multiplier: number 
}

interface CounterIncrementProps {
  delta: number
}

class Counter extends Nullstack<CounterProps> {

  count = 0;

  increment({ delta, multiplier }: NullstackClientContext<CounterProps & CounterIncrementProps>) {
    this.count += delta * multiplier;
  }
  
  render() {
    return (
      <button onclick={this.increment} delta={2}> 
        {this.count}
      </button>
    )
  }

}

export default Counter;

💡 Any attribute with primitive value will be added to the DOM.

✨ Consider using data attributes to make your html valid.

Original Event

The browser default behavior is prevented by default.

You can opt-out of this by declaring a default attribute to the event element.

A reference to the original event is always merged with the function context.

import Nullstack from 'nullstack';

class Form extends Nullstack {

  submit({ event }) {
    event.preventDefault();
  }
  
  render() {
    return (
      <form onsubmit={this.submit} default>
        <button> Submit </button>
      </form>
    )
  }

}

export default Form;

Debounced Events

You can use the attribute debounce passing a number of miliseconds to delay the events of that element

import Nullstack from 'nullstack';

class Counter extends Nullstack {
  
  count = 0

  increment() {
    this.count++
  }
  
  render() {
    return (
      <button onclick={this.increment} debounce={2000}> 
        increment 
      </button>
    )
  }

}

export default Counter;

TypeScript

Stateful Components accept a generic that reflect in the props that its tag will accept

// src/Counter.tsx
import Nullstack, { NullstackClientContext } from 'nullstack';

interface CounterProps {
  multiplier: number 
}

class Counter extends Nullstack<CounterProps> {

  // ...
  
  render({ multiplier }: NullstackClientContext<CounterProps>) {
    return <div> {multiplier} </div>
  }

}

export default Counter;
// src/Application.tsx
import Counter from './Counter'

export default function Application() {
  return <Counter multiplier={4} />
}

Inner components

Instead of creating a new component just to organize code-splitting, you can create an inner component.

Inner components are any method that the name starts with render followed by an uppercase character.

Inner components share the same instance and scope as the main component, therefore, are very convenient to avoid problems like props drilling.

To invoke the inner component use a JSX tag with the method name without the render prefix.

import Nullstack, { NullstackClientContext, NullstackNode } from 'nullstack';

interface CounterProps {
  multiplier: number 
}

interface CounterButtonProps {
  delta: number
}

declare function Button(context: CounterProps): NullstackNode

class Counter extends Nullstack<CounterProps> {

  count = 0;

  increment({ delta, multiplier }: NullstackClientContext<CounterProps & CounterButtonProps>) {
    this.count += delta * multiplier;
  }

  renderButton({ delta = 1 }: NullstackClientContext<CounterProps & CounterButtonProps>) {
    return (
      <button onclick={this.increment} delta={delta}> 
        {this.count}
      </button>
    )
  }
  
  render() {
    return (
      <div>
        <Button />
        <Button delta={2} />
      </div>
    )
  }

}

export default Counter;

💡 Nullstack will inject a constant reference to the function at transpile time in order to completely skip the runtime lookup process!

Next Step

➡️ Learn more about Core Features: Full stack lifecycleHave any questions or suggestions? Join our Discord