> For the complete documentation index, see [llms.txt](https://developers.felt.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developers.felt.com/js-sdk/general-concepts.md).

# General concepts

This guide covers the mental model and the common patterns used throughout the Felt SDK. Understanding these once makes every other page predictable.

## The Felt controller

Everything you do with the SDK goes through a **controller** object — named `felt` in all the examples in these docs. How you get it depends on where your code runs:

* **Embeds** — your code runs in *your* page, and the map lives in an iframe. `Felt.embed(container, mapId, options)` creates the iframe and returns a controller for it. You can also use `Felt.connect(iframe.contentWindow)` to attach a controller to a Felt iframe you've already placed on the page. See [Controlling maps](/js-sdk/controlling-maps.md) and [Embed options](/js-sdk/embed-options.md).
* **Extensions** — your code runs *inside* the Felt app, and receives a ready-made controller: no embedding required. See the [extensions documentation](https://help.felt.com/dashboards-and-apps/extensions).

The controller communicates with the map via message passing, which is why every read is asynchronous (see promises below). The same controller API is available in both contexts.

```typescript
import { Felt } from "@feltmaps/js-sdk";

const felt = await Felt.embed(
  document.getElementById("container"),
  FELT_MAP_ID,
);
```

## Session-only vs persisted changes

A crucial rule for building on the SDK: **changes made via the SDK are visible only to the current session — they are not saved to the map.** This applies to:

* Annotations created with [`createElement`](/js-sdk/drawing-annotations.md)
* Layers created with [`createLayersFromGeoJson`](/js-sdk/working-with-layers.md)
* Style changes made with `setLayerStyle`
* Filters set with [`setLayerFilters`](/js-sdk/layer-filters.md)

This makes the SDK safe for building per-visitor experiences on shared maps: ten visitors can each see their own filters and drawings without affecting each other or the underlying map. To make persistent changes to a map, use the [REST API](/rest-api/getting-started.md).

## Use of promises

All methods in the Felt SDK are asynchronous and return Promises — writes as well as reads. This means you'll need to use `await` or `.then()` when calling them:

```typescript
const layer = await felt.getLayer("layer-1");
felt.getElements().then(elements => {
  console.log(elements);
});
```

## Getting entities

The SDK follows a consistent pattern for getting entities. For each entity type, there are usually two getters:

1. A singular getter for retrieving one entity by ID:

```typescript
const layer = await felt.getLayer("layer-1");
const element = await felt.getElement("element-1");
```

2. A plural getter that accepts constraints for retrieving multiple entities:

```typescript
const layers = await felt.getLayers({ ids: ["layer-1", "layer-2"] });
const legendItems = await felt.getLegendItems({ layerIds: ["layer-1", "layer-2"] });
```

The plural getters also allow you to pass no constraints, in which case they'll return all entities of that type:

```typescript
const layers = await felt.getLayers();
const legendItems = await felt.getLegendItems();
```

### Getters can return null

Singular getters return `null` when the entity doesn't exist, and plural getters can contain `null` entries — always check before using the result:

```typescript
const layer = await felt.getLayer("layer-1");
if (layer) {
  // Layer exists, safe to use
  console.log(layer.visible);
} else {
  console.log("Layer not found");
}
```

### Batch your reads

When you need multiple entities, use the plural methods with constraints rather than making multiple individual calls:

```typescript
// Better approach
const layers = await felt.getLayers({ ids: ["layer-1", "layer-2"] });

// Less efficient approach
const layer1 = await felt.getLayer("layer-1");
const layer2 = await felt.getLayer("layer-2");
```

## Change listeners

Each entity type has a corresponding change listener method following the pattern `on{EntityType}Change`:

```typescript
const unsubscribe = felt.onLayerChange({
  options: { id: "layer-1" },
  handler: ({ layer }) => {
    console.log("Layer updated:", layer);
  }
});
```

There are also various other setters and getters in the Felt SDK that follow this convention as much as possible. For example, selection:

```typescript
const selection = await felt.getSelection();
const unsubscribe = felt.onSelectionChange({
  handler: ({ selection }) => {
    console.log("Selection updated:", selection);
  }
});
```

And layer filters — note that this particular listener receives the filters value directly, with no wrapper object:

```typescript
const filters = await felt.getLayerFilters("layer-1");
await felt.setLayerFilters({
  layerId: "layer-1",
  filters: ["name", "eq", "Jane"],
});
const unsubscribe = felt.onLayerFiltersChange({
  options: { layerId: "layer-1" },
  handler: (filters) => console.log(filters.combined),
});
```

For the full list of listeners and their payloads, see the [Events reference](/js-sdk/events-reference.md).

### Cleanup functions

All change listeners return an unsubscribe function that should be called when you no longer need the listener:

```typescript
const unsubscribe = felt.onLayerChange({
  options: { id: "layer-1" },
  handler: ({ layer }) => {
    console.log("Layer changed:", layer);
  }
});

// Later, when you're done listening:
unsubscribe();
```

This is particularly important in frameworks like React where you should clean up listeners when components unmount:

```typescript
useEffect(() => {
  const unsubscribe = felt.onViewportMove({
    handler: (viewport) => {
      console.log("Viewport changed:", viewport);
    }
  });

  // Clean up when the component unmounts
  return () => unsubscribe();
}, []);
```

### Handler and options structure

Change listeners always take a single object parameter containing both `options` and `handler`. This structure makes it easier to add new options in the future without breaking existing code:

```typescript
// Current API
felt.onElementChange({
  options: { id: "element-1" },
  handler: ({ element }) => { /* ... */ }
});

// If we need to add new options later, no breaking changes:
felt.onElementChange({
  options: { 
    id: "element-1",
    newOption: "value" // Can add new options without breaking existing code
  },
  handler: ({ element }) => { /* ... */ }
});
```

## Entity nodes

When dealing with mixed collections of entities (like in selection events), each entity is wrapped in an `EntityNode` object that includes type information:

```typescript
felt.onSelectionChange({
  handler: ({ selection }) => {
    selection.forEach(node => {
      console.log(node.type);    // e.g., "element", "layer", "feature", ...
      console.log(node.entity);  // The actual entity object
      
      if (node.type === "element") {
        // TypeScript knows this is an Element
        console.log(node.entity.attributes);
      }
    });
  }
});
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://developers.felt.com/js-sdk/general-concepts.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
