---
title: "Sugar, Component Services and expose()"
description: "expose publishes a mounted component's api; use(Component) consumes it anywhere, keyed by the function reference. No string tokens and no providers."
source: https://lemonadejs.com/docs/sugar/
---

# Sugar and component services

Some logic belongs to a *living component*: a notification center owns its
queue and its DOM; a dialog service owns the dialog. Sugar makes one
mounted instance's declared api available to the whole application:

```javascript
html`<${Notifications} expose />`            // the instance publishes its api

// anywhere else, in any module, any component:
import { Notifications } from './notifications';
use(Notifications)?.notify('saved!');        // typed by the import itself
```

`use()` is keyed by the component function. The import *is* the discovery
mechanism: no string tokens, no injector, no provider tree.

`expose` publishes one instance's api app-wide; `use()` reaches it from any component, with no prop drilling.

<!--example-->

```js
import { html, component, use } from 'lemonadejs';

const Notifications = component('notifications', {
    api: { notify: Function, clear: Function },
}, (props, { state }) => {
    const queue = state([]);
    props.ref?.({
        notify: (msg) => (queue.value = [...queue.value, msg]),
        clear: () => (queue.value = []),
    });
    return html`<ul>${() => queue.value.map((m) => html`<li>${m}</li>`)}</ul>`;
});

// another component, could be another module: no props, no context
const SaveButton = () => html`<button
    onclick="${() => use(Notifications)?.notify('saved at ' + new Date().toLocaleTimeString())}">Save</button>`;

const App = () => html`<div>
    <${SaveButton} /> <button onclick="${() => use(Notifications)?.clear()}">clear</button>
    <${Notifications} expose />
</div>`;
```

## Application-wide services

Application-wide services need to reach any part of the application while
keeping their internals private and their lifetime tied to a real instance.
A module-level singleton exposes everything, carries no lifecycle, and
provides no signal of whether it exists yet. v5's `lemonade.set('name',
fn)` / `get` / `dispatch` improved on that, but remained keyed by strings,
with the rename and typo fragility of stringly-typed tokens.

v6's answer is one attribute (`expose`) and one function (`use()`), each
guarantee structural rather than conventional:

```javascript
import { component, html, use } from 'lemonadejs';

export const Notifications = component('notifications', {
    api: { notify: Function, clear: Function },     // the declared boundary
}, (props, { state }) => {
    const queue = state([]);                        // PRIVATE, closed over
    props.ref?.({
        notify: (msg) => (queue.value = [...queue.value, msg]),
        clear: () => (queue.value = []),
    });
    return html`<ul>${() => queue.value.map((m) => html`<li>${m}</li>`)}</ul>`;
});
```

- **Only the declared `api` crosses the boundary.** `expose` copies
  exactly the contract's `api` keys from whatever the component passes to
  `props.ref`; an undeclared method never appears in `use()` (the suite
  passes a `secret` method through `ref` and asserts it does not cross).
  Internals are not hidden by convention; they are closed over and
  unreachable.
- **The key is the function reference.** `use(Notifications)` cannot
  typo, survives renames, and is typed by the same import that locates it.
  Two different component functions can never collide.
- **Lifecycle-bound.** The api is published when the exposed instance
  fulfills its `ref` and withdrawn on unmount; `use()` returns `null`
  before the first and after the second. Existence is a checkable fact,
  not an assumption: `use(X)?.method()` is the idiom.
- **Singletons by definition, guarded.** A second `expose` of the same
  component warns `LJS-501` (last one wins, almost always a bug), and
  `expose` without a declared `api` warns too: an exposure with nothing to
  expose is always a mistake. A caller's own `ref` still runs alongside
  `expose`.

**When *not* to use it.** Sugar is for component-owned *services*:
methods that need a living instance's private reactive state and DOM. For
shared *data* there is no component to own it: use
[store()](/docs/store/) with exported actions. A telling fact: none of the
40 Studio blocks uses `expose`, because blocks are products a host application
composes, and claiming an app-wide singleton slot is the application's
decision, not a library component's.

## In practice

No Studio block exposes itself (see above), so the working examples live
in the engine's own suite (`tests/contract.test.ts`, `tests/memory.test.ts`),
which pins every claim this chapter makes:

```javascript
// the boundary: declared api only
const api = use(Notifications);
typeof api.notify        // 'function'
typeof api.count         // 'function'
api.secret               // undefined: passed to ref, NOT declared, never crosses
```

```javascript
// driving a mounted instance from outside the tree
api.notify('saved!');
api.notify('again');
// the exposing instance rendered both: <li>saved!</li><li>again</li>
```

```javascript
// lifecycle: published while alive, null otherwise
const app = mount(App, root);        // App contains <${Notifications} expose />
use(Notifications)                   // { notify, clear }
app.unmount();
use(Notifications)                   // null: withdrawn, nothing retained
```

The memory suite repeats that last cycle and asserts the singleton map
holds nothing after unmount; an exposed service cannot pin its dead
component (the broader guarantees live in [Destroy](/docs/destroy/)).

## Reference

```javascript
import { component, html, use } from 'lemonadejs';

const Service = component(name, {
    api: { method: Function, ... },      // REQUIRED for expose: the boundary
}, (props, tools) => {
    props.ref?.({ method, ... });        // fulfill the api (extras never cross)
    return html`...`;
});

html`<${Service} expose />`              // publish: one instance, app-wide
use(Service)                             // the api object, or null
use(Service)?.method()                   // the consuming idiom
```

- `use()` returns `null` until the exposed instance is mounted, and again
  after it unmounts.
- Re-exposing warns `LJS-501` (last wins); `expose` without `api` in the
  [contract](/docs/contracts/) warns `LJS-501`.
- A caller-supplied `ref` runs in addition to `expose`.
- Shared application data belongs in [store()](/docs/store/); sugar is for
  services owned by a living component.