# \[IGN-15726\]Project settings in Perspective 8.3 for components

**URL:** <https://forum.inductiveautomation.com/t/ign-15726-project-settings-in-perspective-8-3-for-components/115232>\
**Category:** Module Development\
**Tags:** ignition83, perspective\
**Created:** [April 24, 2026, 10:36am UTC](https://forum.inductiveautomation.com/t/ign-15726-project-settings-in-perspective-8-3-for-components/115232 "2026-04-24T10:36:51Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![torstein](https://avatars.discourse-cdn.com/v4/letter/t/f0a364/32.png) [@torstein](https://forum.inductiveautomation.com/u/torstein)\
**Post date:** [April 24, 2026, 10:36am UTC](https://forum.inductiveautomation.com/t/ign-15726-project-settings-in-perspective-8-3-for-components/115232/1 "2026-04-24T10:36:51Z")

</div>

Hi! We are making a module which add Ignition Perspective components (react components). For this we need some common settings, this could be badge handling, style type, etc.

So I have made a project property view in the designer:

 ![image](https://us1.discourse-cdn.com/inductiveautomation/original/3X/3/2/3264babf14d216042ef29b9e8e76ecae657e8cc6.png)

But I haven’t found a good way to store the data and to get the data down to the component.

ChatGPT suggested to use model delegate to send the data from the gateway to the component.

I use this to set the data:

```python
public ObComponentTypeProjectSettingsEditor(DesignerContext context) {
   super(new GridBagLayout());

   project = context.getProject();
   Optional<Resource> resource = project.getSingletonResource(RESOURCE_TYPE);

   if (resource.isPresent()) {
      try {
         settings = ResourceUtil.decodeOrNull(resource.get(), context.createDeserializer(), ObComponentTypeProjectSettings.class);
      } catch (Exception e) {
         log.error("Failed to decode ObComponentTypeProjectSettings", e);
         settings = new ObComponentTypeProjectSettings();
      }
   } else {
         log.error("Failed to get ObComponentTypeProjectSettings");
         settings = new ObComponentTypeProjectSettings();
  }

…

@Override
public Object commit() {
   settings.setComponentType((String) componentTypeDropdown.getSelectedItem());
   return settings;
}

```

I’m not sure if this works. But the naming of the functions seems promising (project.getSingletonResource) and I hope the commit function stores the resource.

But in the model delegate I have trouble accessing this resource. As the gateway context don’t have “context.getProject”.

So I need some guidance.

1. Is the suggested method to use resource and model delegate the correct way to get the project settings down to the component? Or should we use other methods?

2. How do I store and access this resource?

Thanks in advance!

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [April 24, 2026, 12:35pm UTC](https://forum.inductiveautomation.com/t/ign-15726-project-settings-in-perspective-8-3-for-components/115232/2 "2026-04-24T12:35:45Z")

</div>

I'd create a small, Perspective-authenticated servlet that your client would call when loading.

1. Client starts, loads your BrowserResource.
2. Your BrowserResource queries your endpoint and is given the settings for the current project.
3. Store the settings globally in the client, and use as needed in your components.

---

<div class="post-metadata">

**Author:** ![torstein](https://avatars.discourse-cdn.com/v4/letter/t/f0a364/32.png) [@torstein](https://forum.inductiveautomation.com/u/torstein)\
**Post date:** [April 24, 2026, 12:48pm UTC](https://forum.inductiveautomation.com/t/ign-15726-project-settings-in-perspective-8-3-for-components/115232/3 "2026-04-24T12:48:05Z")

</div>

That sounds smart. Like setting window.mymodule.style = "flat" and accessing that from the component? Would that work in the designer?

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [April 24, 2026, 1:12pm UTC](https://forum.inductiveautomation.com/t/ign-15726-project-settings-in-perspective-8-3-for-components/115232/4 "2026-04-24T13:12:43Z")

</div>

> [@torstein](#):
>
> Like setting window.mymodule.style = "flat" and accessing that from the component?

Exactly.

> [@torstein](#):
>
> Would that work in the designer?

Yup, nothing special is needed in the designer.

This is how I handle it in Embr:

> <https://github.com/mussonindustrial/embr/blob/b9d16a202c0469936a0b4d47825a633c5e671364/libraries/javascript/perspective-client/src/globals/globals.ts#L9-L29>

Different modules add different things in that namespace as needed.

The only "gotcha" I've found is that you sometimes need to wait for the `ClientStore` to become available before you can reliably reference it.

> <https://github.com/mussonindustrial/embr/blob/b9d16a202c0469936a0b4d47825a633c5e671364/modules/periscope/web/src/client.ts#L35>

> <https://github.com/mussonindustrial/embr/blob/8.1/main/libraries/javascript/perspective-client/src/utils/waitForClientStore/waitForClientStore.ts#L7>

---

<div class="post-metadata">

**Author:** ![paul-griffith](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/paul-griffith/32/37249_2.png) [@paul-griffith](https://forum.inductiveautomation.com/u/paul-griffith)\
**Post date:** [April 24, 2026, 4:20pm UTC](https://forum.inductiveautomation.com/t/ign-15726-project-settings-in-perspective-8-3-for-components/115232/5 "2026-04-24T16:20:22Z")

</div>

Ben also asked for the ability to provide session props in a module, which is something we should do, but no timeline on when:

> [@\[IGN-15726\]Adding module-specific session properties](https://forum.inductiveautomation.com/t/ign-15726-adding-module-specific-session-properties/104456):
>
> Is there any supported way of adding to the built in Session properties (i.e session.props, not session.custom)? I have some configuration stuff that would most naturally fit as session properties (to allow for binding support). I guess I can read/write to whatever session property keys I want, but I’d really like to provide schema information.

---

<div class="post-metadata">

**Author:** ![torstein](https://avatars.discourse-cdn.com/v4/letter/t/f0a364/32.png) [@torstein](https://forum.inductiveautomation.com/u/torstein)\
**Post date:** [April 24, 2026, 7:12pm UTC](https://forum.inductiveautomation.com/t/ign-15726-project-settings-in-perspective-8-3-for-components/115232/6 "2026-04-24T19:12:16Z")

</div>

@paul-griffith for us we would like to have a project setting (which is the same for all sessions). I think I only need a guidance on how to store a setting from the "Project Properties" view in the designer (as shown above) and how to retrieve that setting in an Gateway endpoint.

---

<div class="post-metadata">

**Author:** ![paul-griffith](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/paul-griffith/32/37249_2.png) [@paul-griffith](https://forum.inductiveautomation.com/u/paul-griffith)\
**Post date:** [April 24, 2026, 7:59pm UTC](https://forum.inductiveautomation.com/t/ign-15726-project-settings-in-perspective-8-3-for-components/115232/7 "2026-04-24T19:59:26Z")

</div>

Yeah - to me, the "natural" way to do that that end users would have a good time with (if they need to bind to it or otherwise read it easily) would be a read-only session property that your module is in charge of providing.

Ben's workaround(s) are probably the best solution in the short term - he's got more practical experience extending Perspective than anyone else I'm aware of.

Our solution to the same problem first party is simple - we just made the requisite changes first party in the page loading, and didn't think about the use case of third parties needing to extend things.

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [April 24, 2026, 8:10pm UTC](https://forum.inductiveautomation.com/t/ign-15726-project-settings-in-perspective-8-3-for-components/115232/8 "2026-04-24T20:10:20Z")

</div>

> [@paul-griffith](#):
>
> workaround(s)

100%, the way I suggested is just a work around.  
At the moment the only thing easy about extending Perspective is adding components. Beyond that, it gets hacky pretty quickly.

> [@paul-griffith](#):
>
> read-only session property

I’d like to see the access be controllable.

Some properties would benefit from read/write (similar to `session.props.theme`), while others only need read access (similar to `session.props.id`).

---

<div class="post-metadata">

**Author:** ![torstein](https://avatars.discourse-cdn.com/v4/letter/t/f0a364/32.png) [@torstein](https://forum.inductiveautomation.com/u/torstein)\
**Post date:** [April 25, 2026, 6:05am UTC](https://forum.inductiveautomation.com/t/ign-15726-project-settings-in-perspective-8-3-for-components/115232/9 "2026-04-25T06:05:52Z")

</div>

I think I solved the project setting issue. But it feels very hacky

```java
        String projectName = component.getSession().getProjectName();
        Optional<RuntimeResourceCollection> resourceCollection = gatewayContext.getProjectManager().find(projectName);
        Optional<Resource> resource = resourceCollection.get().getSingletonResource(ObComponentTypeProjectSettings.RESOURCE_TYPE);
        return ResourceUtil.decodeOrNull(resource.get(), gatewayContext.createDeserializer(), ObComponentTypeProjectSettings.class);

```

Is there a better way to get the project resource collection than:

```java
Optional<RuntimeResourceCollection> resourceCollection = gatewayContext.getProjectManager().find(projectName);
        Optional<Resource> resource = resourceCollection.get().getSingletonResource(ObComponentTypeProjectSettings.RESOURCE_TYPE);

```

Now I'm depending on the naming convention of the resource collection. The code will fail, if the mapping between project name and collection name changes.

---

<div class="post-metadata">

**Author:** ![paul-griffith](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/paul-griffith/32/37249_2.png) [@paul-griffith](https://forum.inductiveautomation.com/u/paul-griffith)\
**Post date:** [April 27, 2026, 6:53pm UTC](https://forum.inductiveautomation.com/t/ign-15726-project-settings-in-perspective-8-3-for-components/115232/10 "2026-04-27T18:53:17Z")

</div>

> [@torstein](#):
>
> mapping between project name and collection name changes

A resource collection and a project are synonymous; they can't ever get out of sync.  
'Resource collection' is just our invented nomenclature because we needed something to describe the root of the hierarchy that contains both projects and deployment modes, since both are modeled in the same way in 8.3+.  
So a project is a resource collection, but not all resource collections are projects - squares and rectangles. If that helps.

As a aside:

> [@torstein](#):
>
> `ResourceUtil.decodeOrNull`

I would recommend moving away from `ResourceUtil.decodeOrNull` in new code. I'd recommend you adjust your editor/workspace to serialize using GSON/JSON and perform the inverse operation against the stored resource data. Better for Git compatibility/human readability than the XML/Java deserialization route that is used automatically.

---

<div class="post-metadata">

**Author:** ![torstein](https://avatars.discourse-cdn.com/v4/letter/t/f0a364/32.png) [@torstein](https://forum.inductiveautomation.com/u/torstein)\
**Post date:** [April 27, 2026, 7:17pm UTC](https://forum.inductiveautomation.com/t/ign-15726-project-settings-in-perspective-8-3-for-components/115232/11 "2026-04-27T19:17:51Z")

</div>

Thank you for the guidance! 🙏

> [@paul-griffith](#):
>
> I would recommend moving away from `ResourceUtil.decodeOrNull` in new code. I'd recommend you adjust your editor/workspace to serialize using GSON/JSON and perform the inverse operation against the stored resource data. Better for Git compatibility/human readability than the XML/Java deserialization route that is used automatically.

I'm not a java developer, so do you have any example code or javadoc to guide me on how to do this serialization using json?

---

<div class="post-metadata">

**Author:** ![paul-griffith](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/paul-griffith/32/37249_2.png) [@paul-griffith](https://forum.inductiveautomation.com/u/paul-griffith)\
**Post date:** [April 27, 2026, 9:39pm UTC](https://forum.inductiveautomation.com/t/ign-15726-project-settings-in-perspective-8-3-for-components/115232/12 "2026-04-27T21:39:11Z")

</div>

Eh...If you're settled on using a project property window, as in your first screenshot, it's more or less pointless. You can override `serialize` and `deserialize` from `AbstractPropertyEditorPanel` to use GSON, which is what Perspective does in `com.inductiveautomation.perspective.designer.project.properties.AbstractPerspectivePropsPanel`:

 ![image](https://us1.discourse-cdn.com/inductiveautomation/original/3X/f/b/fb02a573fa29afd8ea17607576f3aa3dd06a8cdf.png)

But you can't actually change where the data gets stored, so it'll still be written as a `data.bin` file underneath the resource, even though it's perfectly readable UTF-8 JSON:

 ![image](https://us1.discourse-cdn.com/inductiveautomation/original/3X/8/0/804e98e70beb60a9b0ea5084623e539459e2ccfe.jpeg)

So ignore me unless you want to add a fair bit of work and move to a distinct resource node in the tree.

---

<div class="post-metadata">

**Author:** ![ynejati](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/ynejati/32/6093_2.png) [@ynejati](https://forum.inductiveautomation.com/u/ynejati)\
**Post date:** [April 28, 2026, 4:13pm UTC](https://forum.inductiveautomation.com/t/ign-15726-project-settings-in-perspective-8-3-for-components/115232/13 "2026-04-28T16:13:18Z")

</div>

Lots of good suggestions here. This is how I would do it. Two slightly different approaches here. I would go with the singleton approach (see alternative at the bottom). After registering the custom project panel in the DesignerHook:

## 1. Gateway — ComponentModelDelegate

The `ComponentModelDelegate` is created per component instance. Load the config by asking the `ProjectManager` for the project's resource collection, then push it down to the client over the WebSocket on startup.

```java
public class MyComponentModelDelegate extends ComponentModelDelegate {

    private static final String EVENT_CONFIG_UPDATE = "config-update";

    private final GatewayContext gatewayContext;
    private final Gson gson = new Gson(); // or your module's configured Gson

    public MyComponentModelDelegate(Component component, GatewayContext gatewayContext) {
        super(component);
        this.gatewayContext = gatewayContext;
    }

    @Override
    protected void onStartup() {
        getConfig().ifPresent(config -> {
            JsonObject payload = gson.toJsonTree(config).getAsJsonObject();
            fireEvent(EVENT_CONFIG_UPDATE, payload);
        });
    }

    @Override
    public void handleEvent(EventFiredMsg message) {
        // handle any events fired by the client-side delegate if needed
    }

    private Optional<CustomComponentConfig> getConfig() {
        String projectName = component.getSession().getProjectName();
        return gatewayContext.getProjectManager()
            .getProject(projectName)
            .flatMap(project -> {
                Resource resource = project.getResourceCollection()
                    .getResource(CustomComponentConfig.RESOURCE_TYPE);
                if (resource == null) return Optional.empty();
                String json = new String(resource.getData(), StandardCharsets.UTF_8);
                return Optional.of(gson.fromJson(json, CustomComponentConfig.class));
            });
    }
}

```

### Registering the delegate factory in your GatewayHook

```java
@Override
public void startup(GatewayContext context, ...) {
    PerspectiveContext perspectiveContext = PerspectiveContext.get(context);
    perspectiveContext.getComponentRegistry()
        .registerComponentModelDelegateFactory(
            MyComponent.COMPONENT_ID,
            comp -> new MyComponentModelDelegate(comp, context)
        );
}

```

### Reacting to project saves

If you need the delegate to re-push updated config when the project is saved without restarting the component, subscribe to the `PerspectiveProjectCache` event bus:

```java
@Override
protected void onStartup() {
    PerspectiveContext.get(gatewayContext)
        .getProjectCache()
        .getEventBus()
        .register(this);
    pushConfig();
}

@Override
protected void onShutdown() {
    PerspectiveContext.get(gatewayContext)
        .getProjectCache()
        .getEventBus()
        .unregister(this);
}

@Subscribe
public void onProjectUpdated(ProjectUpdatedEvent event) {
    if (event.project.name.equals(component.getSession().getProjectName())) {
        pushConfig();
    }
}

private void pushConfig() {
    getConfig().ifPresent(config -> {
        JsonObject payload = gson.toJsonTree(config).getAsJsonObject();
        fireEvent(EVENT_CONFIG_UPDATE, payload);
    });
}

```

* * *

## 2. Client — ComponentStoreDelegate

Create a TypeScript class that extends `ComponentStoreDelegate`. Implement `handleEvent` to receive events from the gateway delegate, store the data locally, and call `this.notify()` to trigger a re-render. Expose the data to React via `mapStateToProps`.

```typescript
import { ComponentStoreDelegate, AbstractUIElementStore, JsObject } from '@inductiveautomation/perspective-client';
import { bind } from 'bind-decorator';

interface CustomComponentConfig {
    someValue: string;
    someList: string[];
}

interface MyDelegateState {
    config: CustomComponentConfig | null;
}

export class MyComponentStoreDelegate extends ComponentStoreDelegate {
    private config: CustomComponentConfig | null = null;

    constructor(component: AbstractUIElementStore) {
        super(component);
    }

    @bind
    handleEvent(eventName: string, eventObject: JsObject): void {
        switch (eventName) {
            case 'config-update':
                this.config = eventObject as CustomComponentConfig;
                this.notify(); // triggers mapStateToProps → component re-renders
                break;
            default:
                console.warn(`Unhandled delegate event: ${eventName}`);
        }
    }

    @bind
    mapStateToProps(): MyDelegateState {
        return { config: this.config };
    }
}

```

### Wiring into ComponentMeta

```typescript
import { ComponentMeta, AbstractUIElementStore, ComponentStoreDelegate } from '@inductiveautomation/perspective-client';

export class MyComponentMeta implements ComponentMeta {
    getComponentType(): string {
        return 'your.module.id.my-component';
    }

    getViewComponent(): PComponent {
        return MyComponent;
    }

    createDelegate(component: AbstractUIElementStore): ComponentStoreDelegate | undefined {
        return new MyComponentStoreDelegate(component);
    }

    getPropsReducer(tree: PropertyTree): MyProps {
        return {
            // map PropertyTree to props as normal
        };
    }
}

```

### Consuming delegate state in your React component

The `ComponentStore` merges your `mapStateToProps()` return value into `props.delegate`. Type it via the second generic parameter on `ComponentProps`.

```typescript
import { ComponentProps, PComponent } from '@inductiveautomation/perspective-client';

type MyProps = {
    // your normal component props from the property tree
};

export class MyComponent extends PComponent<ComponentProps<MyProps, MyDelegateState>> {
    render() {
        const { config } = this.props.delegate ?? {};

        if (!config) {
            return <div>Loading...</div>;
        }

        return (
            <div>
                <span>{config.someValue}</span>
            </div>
        );
    }
}

```

* * *

## Data Flow Diagram

```python
Designer (Swing)
  AbstractPropertyEditorPanel
    initProps() ← deserialize() ← project resource bytes
    setChanged() ← user edits
    commit() → serialize() → project resource bytes
         │
         │ project save
         ▼
Gateway resource collection
         │
         │ component startup / project update
         ▼
ComponentModelDelegate.onStartup()
  getConfig() reads resource bytes → CustomComponentConfig
  fireEvent("config-update", payload)
         │
         │ WebSocket
         ▼
ComponentStoreDelegate.handleEvent("config-update", payload)
  this.config = payload
  this.notify()
         │
         ▼
mapStateToProps() → { config }
         │
         ▼
props.delegate.config → React render

```

* * *

## Alternative: Singleton Component + Window Store

Instead of wiring each component with its own `ComponentModelDelegate`, you can designate a single **singleton component** that owns the gateway connection and publishes received config into a shared store mounted on `window`. Other components read from the store and subscribe to changes using the `subscriptionFactory` API.

This trades per-component WebSocket wiring for a single connection point with a shared observable. It's a good fit when many components need the same config and you don't want each one to independently negotiate a delegate lifecycle.

### The window store

Create a store class that holds the config and exposes the subscription API. Mount it on `window` once so all components share the same instance.

```typescript
// CustomComponentStore.ts
import {
    createSubscriptionMap,
    subscriptionFactory,
    SubscriptionHandler,
} from '@inductiveautomation/perspective-client';

interface CustomComponentConfig {
    someValue: string;
    someList: string[];
}

const STATE_CONFIG = 'config';

class CustomComponentStore {
    private config: CustomComponentConfig | null = null;
    private subscriptionMap = createSubscriptionMap([STATE_CONFIG]);
    public subscribe: SubscriptionHandler;
    private notify: (state?: string) => void;

    constructor() {
        const { subscribe, notify } = subscriptionFactory(
            this.subscriptionMap,
            'CustomComponentStore'
        );
        this.subscribe = subscribe;
        this.notify = notify;
    }

    setConfig(config: CustomComponentConfig): void {
        this.config = config;
        this.notify(STATE_CONFIG); // notify subscribers of the 'config' state key
    }

    getConfig(): CustomComponentConfig | null {
        return this.config;
    }
}

declare global {
    interface Window {
        customComponentStore: CustomComponentStore;
    }
}

if (!window.customComponentStore) {
    window.customComponentStore = new CustomComponentStore();
}

export const store = window.customComponentStore;

```

### Singleton component delegate

The singleton component's `ComponentStoreDelegate` is the only place that receives events from the gateway and writes to the store. No other component needs a delegate for config.

```typescript
import { ComponentStoreDelegate, AbstractUIElementStore, JsObject } from '@inductiveautomation/perspective-client';
import { store } from './CustomComponentStore';

export class SingletonComponentStoreDelegate extends ComponentStoreDelegate {
    constructor(component: AbstractUIElementStore) {
        super(component);
    }

    handleEvent(eventName: string, eventObject: JsObject): void {
        switch (eventName) {
            case 'config-update':
                store.setConfig(eventObject as CustomComponentConfig);
                break;
        }
    }
}

```

The gateway side (`ComponentModelDelegate`) is identical to the approach in section 1 — it pushes `fireEvent("config-update", payload)` on startup and on project save. The only difference is that only the singleton component type needs a delegate registered, not every component type.

### Consuming components

Other components read from the store directly and subscribe to changes in their React lifecycle.  
The disposer returned by `subscribe` must be called on unmount to avoid memory leaks.

```typescript
import { store } from './CustomComponentStore';

export class OtherComponent extends PComponent<ComponentProps<OtherProps>> {
    private unsubscribe?: () => void;

    componentDidMount() {
        // subscribe to the 'config' state key specifically
        this.unsubscribe = store.subscribe(() => {
            this.forceUpdate();
        }, 'config');
    }

    componentWillUnmount() {
        this.unsubscribe?.();
    }

    render() {
        const config = store.getConfig();
        if (!config) return <div>Loading...</div>;
        return <div>{config.someValue}</div>;
    }
}

```

To subscribe to **any** change on the store (not a specific state key), omit the second argument:

```typescript
this.unsubscribe = store.subscribe(() => {
    this.forceUpdate();
});

```

The `subscriptionFactory` will route this to the `ANY_CHANGE_KEY` listeners, which are notified on every `notify()` call regardless of which state key changed.

### Data flow diagram

```python
Gateway delegate (singleton component only)
  fireEvent("config-update", payload)
         │
         │ WebSocket
         ▼
SingletonComponentStoreDelegate.handleEvent()
  store.setConfig(payload)
    → this.notify('config')
         │
         ├──→ OtherComponent A subscriber fires → forceUpdate()
         ├──→ OtherComponent B subscriber fires → forceUpdate()
         └──→ OtherComponent C subscriber fires → forceUpdate()
                    ↓
              store.getConfig() → render

```

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [April 28, 2026, 4:28pm UTC](https://forum.inductiveautomation.com/t/ign-15726-project-settings-in-perspective-8-3-for-components/115232/14 "2026-04-28T16:28:58Z")

</div>

Bluntly, using a `ComponentModelDelegate` is a terrible way of getting global state into a Perspective client. It's far more complicated than reading an endpoint, and introduces weird dependencies between components and settings.

The custom store is neat idea though, I'll have to play with the `SubscriptionHandler` stuff sometime.

---

<div class="post-metadata">

**Author:** ![ynejati](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/ynejati/32/6093_2.png) [@ynejati](https://forum.inductiveautomation.com/u/ynejati)\
**Post date:** [April 28, 2026, 6:09pm UTC](https://forum.inductiveautomation.com/t/ign-15726-project-settings-in-perspective-8-3-for-components/115232/15 "2026-04-28T18:09:47Z")

</div>

Right. You can put the singleton shadow component in a dock that is shared across the application. It will reliably get notified when there are updates, and notify any subscribers of updates. There would be no need to poll an endpoint for updates (though there's noting really wrong with that if it's just one entity doing the polling). It will also reliably initialize on its own, versus having to poll the client store.

Perhaps the best solution would be to combine both solutions.

- A servlet endpoint with cache busting hash.
- A front-end singleton initialized project properties store in your component module index file that polls this endpoint for updates, and uses our Subscription API.
- A custom reusable front-end React hook that makes this subscription easy.
- Custom components that make use of this custom React hook.

Good luck, Torstein. Let us know how it goes!

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [April 28, 2026, 6:18pm UTC](https://forum.inductiveautomation.com/t/ign-15726-project-settings-in-perspective-8-3-for-components/115232/16 "2026-04-28T18:18:33Z")

</div>

> [@ynejati](#):
>
> There would be no need to poll an endpoint for updates

There's no need to poll.

Add a handler to the Page via `clientStore.connection.handlers`, and trigger it from the Gateway via `PageModel.send` when your resource changes.

> **[IOException - PageModel](https://sdk.inductiveautomation.com/javadoc/ignition81/8.1.33/com/inductiveautomation/perspective/gateway/model/PageModel.html#send(java.lang.String,com.inductiveautomation.ignition.common.gson.JsonElement))**
>
> declaration: package: com.inductiveautomation.perspective.gateway.model, class: PageModel

You could also distribute the new settings over that same connection too, but you’d have to come up with a way of making the first load happen.

EDIT:  
It would be nice to have the ability to register Page Handlers on the Gateway side. It currently requires reflection:

> <https://github.com/mussonindustrial/embr/blob/b9d16a202c0469936a0b4d47825a633c5e671364/libraries/perspective/gateway/src/main/kotlin/com/mussonindustrial/embr/perspective/gateway/reflect/PageModel.kt#L7-L9>

---

<div class="post-metadata">

**Author:** ![ynejati](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/ynejati/32/6093_2.png) [@ynejati](https://forum.inductiveautomation.com/u/ynejati)\
**Post date:** [April 28, 2026, 6:48pm UTC](https://forum.inductiveautomation.com/t/ign-15726-project-settings-in-perspective-8-3-for-components/115232/17 "2026-04-28T18:48:07Z")

</div>

Using the WS channel is a good idea too.

If you're at the ICC this year, come find me. I'd like to hear your thoughts.

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [April 28, 2026, 6:55pm UTC](https://forum.inductiveautomation.com/t/ign-15726-project-settings-in-perspective-8-3-for-components/115232/18 "2026-04-28T18:55:34Z")

</div>

It’s not confirmed yet, but I should be there!

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [June 4, 2026, 2:21pm UTC](https://forum.inductiveautomation.com/t/ign-15726-project-settings-in-perspective-8-3-for-components/115232/19 "2026-06-04T14:21:57Z")

</div>

> [@ynejati](#):
>
> If you're at the ICC this year, come find me. I'd like to hear your thoughts.

I’ve got my ticket, see ya there 😉

---

<div class="post-metadata">

**Author:** ![ynejati](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/ynejati/32/6093_2.png) [@ynejati](https://forum.inductiveautomation.com/u/ynejati)\
**Post date:** [June 4, 2026, 9:02pm UTC](https://forum.inductiveautomation.com/t/ign-15726-project-settings-in-perspective-8-3-for-components/115232/20 "2026-06-04T21:02:44Z")

</div>

Awesome! I'll bring the biggest notepad I can find. 😅
