Repository: storybook
Issue: [Tracking]: Universal State Primitive 📡
Labels: Tracking

## 🧑‍🤝‍🧑 Who: @JReinhold and @valentinpalkovic

This is a tracking issue for a project centered around creating a primitive that makes it easier to have state that is synced between some or all "environments".


# ⚠️ Problem

Storybook consists of multiple "environments" that tie the experience together:

- The dev server
- One or multiple places in the the manager UI (eg. an addon panel and toolbar dropdown)
- The preview UI (ie. in stories)

The Channel API can be used to communicate between these environments, but it only solves the _communication_ part, and leaves it up to the consumer to figure out how to keep state in sync between the environments, which is not straight forward. A few use cases that are harder to implement than they should be today are:

1. Sharing state between the dev server and the manager UI. The (experimental) Test addon does this a lot, sharing information between the Vitest instance on the server with the sidebar UI.
2. Sharing state between multiple "slots" in the manager UI, ie. an addon panel that shares information with a toolbar tool. This is hard today because the two components are registered separately without any possibility to instantiate a React context provider or similar way to share information.
3. Sharing state between the preview and the manager UI, ie. an addon panel that needs to show different information based on some logic it is injecting into stories.

The most immediate problem we're facing _today_ is inconsistent state in the Test Module. It can quickly get out of sync if the dev server restarts, if the manager has stale state in session storage, or if other tabs changes the state of the server.

# 🏁 Goals

The high-level goals of this project is to:

1. Construct a low-level primitive API that makes it easier to share state across multiple environments. It must be use-case agnostic, not focusing on any specific environments, eg. it shouldn't _need_ to sync to the server, if the use case is only to sync between manager UI slots. It's still TBD whether or not that API will be a public, non-experimental at the end of the project or not, it might be beneficial to keep it internal/experimental for a period of time before we commit to stabilising it for public use.
2. Use the new API to revamp the state management in the testing addon, eliminating state inconsistencies where possible. There's a high chance this supersedes the current (experimental) testing module API, still TBD.

# 📚 Research

@JReinhold has done an initial POC on a "UniversalState" API, that can serve as an inspiration or starting point for discussions and initial implementation. The final API might not look like this at all, or it might be a slight modification of this, we'll see. 🤷 The POC is implemented in the [`jeppe/universal-state`](https://github.com/storybookjs/storybook/commit/7bca712b843d2af0e8c374563bb2f3fec816ee66) branch.

(note the following videos where originally intended for the Storybook Core team eyes only, so it might require contextual knowledge that you don't have)

<details><summary>Demonstration of UniversalState POC from the consumer perspective</summary>

https://github.com/user-attachments/assets/34991255-1750-48a6-9563-7da6c2aef28d

https://github.com/user-attachments/assets/ac090169-c522-40cb-8866-1f53865d2ec2

</details> 

<details><summary>Demonstration of how the UniversalState POC is implemented</summary>

https://github.com/user-attachments/assets/e39b5fd5-13e6-4b83-b243-78ccf86ff99c

</details> 

The `@chromatic-com/storybook` addon also has a concept of [`SharedState`](https://github.com/chromaui/addon-visual-tests/blob/main/src/utils/SharedState.ts) that is very similar to the UniversalState POC as it needs to solve the same underlying pain-points.

# 🚩 Milestones

The base idea is currently to create a minimal, low-level abstraction on top of the existing Channel API that handles the syncing of the state - the Channel API already handled _communication_ between the environments, but it doesn't handle anything regarding keeping state in sync.

## Primitive API

```[tasklist]
### Tasks
- [x] Discuss confict handling scenarios
- [ ] Revive UniversalState POC within `@storybook/core`
- [ ] Support a Selector-pattern when subscribing to the state
- [ ] Add React-hook in manager UI that handles re-rendering on state change
- [ ] Handle conflict resolution when multiple state instances modify state simultaneously
- [ ] Handle conflicts when multiple state instances are instantiated simultaneously
- [ ] Add persistence hooks for consumers to read/write persisted state
```

## Use API in testing addon

```[tasklist]
### Tasks
- [ ] Use UniversalStore to sync configs (a11y, coverage)
- [ ] Use UniversalStore to sync watch mode
- [ ] Use UniversalStore to sync running state and triggering runs
```