state

The state() function creates a reactive variable with enhanced primitive-like behavior and returns a getter/setter pair. It enables you to store, read, and update local data with automatic DOM reactivity when values change.

⚠️ Convention: In <script> blocks, always use .value when working with primitive states. This is because JavaScript Proxies cannot fully emulate primitive assignment.
βœ… In Mustache templates {{ ... }}, .value is not required.

Use Cases

Use state() when you want to bind dynamic data in your templates or react to changes with effect(). It's perfect for form fields, counters, toggles, and local variables scoped to a section or component.

Syntax

const [count, setCount] = pphp.state(0);
// or initialize without destructuring (read-only)
// Note: When using the read-only form, you cannot update the state or use it with effect methods that require a setter.
pphp.state('theme', 'light');
  • key: A string that uniquely identifies the state variable. Must not collide with reserved native objects.
  • initial: The initial value of the state (can be any type: string, number, boolean, object, etc).
  • Returns: A tuple [getter, setter] where:
    • getter() returns the current value (for reactivity) and can be used directly in bindings.
    • setter(value) updates the value, or accepts a function for functional updates.

Access Rules

Context Primitive (string, number, boolean) Object / Array Requires .value?
{{ mustache bindings }} βœ… Works βœ… Works No
console.log() βœ… Prints value directly βœ… Prints value directly No
<script> (arithmetic, if, comparisons) ❌ Requires .value βœ… Direct access Yes (primitives only)
Strict checks (===, typeof, instanceof) ❌ Requires .value ❌ Requires .value Yes
πŸ“Œ Best Practice: β€’ In templates, forget about .value, everything works naturally.
β€’ In <script> blocks, always use .value for primitives (string, number, boolean). β€’ Objects/arrays work without .value for property access, but still require it for JSON / type methods.

Examples

<div>
  <p>Hello, {{ name }}</p>
  <p>Count: {{ count }}</p>
</div>

<script>
  const [name, setName] = pphp.state('John');
  const [count, setCount] = pphp.state(0);

  export const test = () => {
    // βœ… Template literals & logging
    console.log(`Hello ${name}`);  // Works without .value
    console.log(count);            // Logs primitive directly

    // ⚠️ In script: use .value
    if (count.value > 5) {
      console.log('Count is greater than 5');
    }

    // βœ… Update
    setCount(prev => prev + 1);

    // ⚠️ Strict equality
    if (name.value === 'John') {
      console.log('Exact match');
    }
  };
</script>

Objects / Arrays

<div>
  <p>User: {{ user.name }}</p>
  <p>First item: {{ items[0] }}</p>
</div>

<script>
  const [user, setUser] = pphp.state({ id: 1, name: 'Abraham' });
  const [items, setItems] = pphp.state(['apple', 'banana']);

  console.log(user.name);   // Works without .value
  console.log(items[0]);    // Works without .value

  // ❌ Requires .value for inspection
  console.log(Object.keys(user.value));
  console.log(Array.isArray(items.value));
</script>
πŸš€ Summary: β€’ Mustache templates = no .value. β€’ Console logging = no .value. β€’ Script operations = primitives must use .value. β€’ Objects/arrays mostly work without .value, except for type/inspection methods.

⚠️ Requires .value

  • β€’ Strict equality: name.value === 'John'
  • β€’ Type checking: typeof count.value
  • β€’ Instance checking: user.value instanceof Object
  • β€’ JSON methods: JSON.stringify(user.value)
  • β€’ Array methods: Array.isArray(items.value)
  • β€’ Object methods: Object.keys(user.value)

🎯 Best Practice

Use the enhanced primitive-like behavior for natural JavaScript operations (85% of use cases). Only use .value when you need strict equality, type checking, or specific built-in functions. In Mustache templates, everything works automatically.

getState()

pphp.getState(key) retrieves a scoped reactive value by key. It first searches the current component scope, then walks up the parent scopes, and finally falls back to shared state (pphp.share). The return value is a callable + proxy: you can call() it to read, use dot-notation to access properties, and update via .set().

πŸ’‘ If the state appears a bit later (e.g., created by another script), a short delayed check will detect it and trigger a re-render automatically.

Signature

getState<T = any>(key: string): T
  & ((...args: any[]) => T)
  & { set: (value: T | ((prev: T) => T)) => void }

Lookup order

  • Current scope: ComponentA.ComponentB.key
  • Parents (upwards): ComponentA.key, then key
  • Shared: if registered via pphp.share('key', ...)

Basic usage

<script>
  // Grab reactive state (scoped/parent/shared)
  const isValid = pphp.getState('isValid');

  // βœ… Read (callable)
  console.log('valid?', isValid());

  // βœ… Update (direct or functional)
  isValid.set(true);
  isValid.set(prev => !prev);

  // βœ… In objects: dot notation is reactive
  const form = pphp.getState('form');
  console.log(form.user.name);   // property access
  form.user.name = 'Abraham';    // property write triggers update

  // ⚠️ PRIMITIVES in <script>: use .value for strict checks
  if (isValid.value === true) {
    console.log('Strict check passed');
  }
</script>

With templates

In Mustache bindings, you can use the state naturally (no .value needed).

<div class="text-sm">
  Status: {{ isValid ? 'Valid' : 'Invalid' }}
  <br />
  User: {{ form.user.name }}
</div>

<script>
  const isValid = pphp.getState('isValid');
  const form    = pphp.getState('form');
</script>

Scope awareness example

When called from a child component, getState('key') first checks the child’s scope (e.g., Parent.Child.key), then the parent (Parent.key), then global (key), then shared.

<!-- Parent sets form -->
<script>
  const form = pphp.getState('form');
  form.user = { id: 1, name: 'Ada' };
</script>

<!-- Child reads same key; resolves up to parent if not present locally -->
<script>
  const form = pphp.getState('form');
  console.log(form.user.name); // 'Ada'
</script>

Initialize after hydration

Combine with pphp.onHydrationComplete() to run code only when reactive values are guaranteed ready.

<script>
  const isValid = pphp.getState('isValid');
  const form    = pphp.getState('form');

  pphp.onHydrationComplete(() => {
    // Safe to read/measure or seed defaults
    if (!form.user?.name) {
      form.user = { id: 1, name: 'Abraham' };
    }
    if (isValid() == null) {
      isValid.set(false);
    }
  });
</script>

Behavior notes

  • Callable read: state() β†’ current value; reactivity is tracked.
  • Dot-notation: If the value is an object, its properties are proxied and reactive.
  • Updates: state.set(value) or functional state.set(prev => ...).
  • Late discovery: If the key isn’t found yet, a short retry window watches for it and triggers a flush when it appears.
  • Primitives in