OpenProject is the leading open source project management software.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
openproject/docs/development/concepts/state-management/README.md

166 lines
5.6 KiB

---
sidebar_navigation:
title: State management
description: Get an overview of how frontend state management works
robots: index, follow
keywords: state management, stores, input states
---
# Development concept: State management
State management in complex frontend applications is a topic that has been heavily evolving over the past years. Redux and stores, one-way data flow are all the rage nowadays. OpenProject is an old application, so its frontend exists way before these concepts were introduced and became popular.
## Key takeaways
*State managent in OpenProject frontend...*
- is mainly controlled by `RxJs` and the reactivestates library
- `State` and `InputState` are mostly syntactic sugar over RXJS `Subject` and `BehaviorSubject`
- States are used to hold and cache values with their values and non-values being observable
## InputState
An `InputState` object is a wrapper around RXJS Behaviorsubject. It provides some syntactic sugar over it to inspect values and provide helpers to observe streams and fill in the underlying `Subject`.
To create an InputState, you call `new InputState<Type>(initialValue:Type|undefined)` or use the helper method `input<Type>(initialValue?:Type)` which will fall back to the undefined value. You will then be able to inspect its value and contents.
```typescript
// An initially empty state
const state = new InputState<string>();
console.log(state.isPristine()); // true
console.log(state.hasValue()); // false
console.log(state.value); // undefined
```
An InputState will hold exactly one value, that you can fill explicitly:
```typescript
state.putValue('my value');
console.log(state.isPristine()); // false
console.log(state.hasValue()); // true
console.log(state.value); // 'my value'
```
You can find out if a value is older than a specific amount of ms:
```typescript
state.isValueOlderThan(60000); // Value is older than 60 seconds?
```
The value can be explicitly cleared with `state.clear()`.
You can also fill the InputState with the result of a promise request. With `putFromPromiseIfPristine`, the promise will only be requested if the state is empty. This is useful for performing API requests that should not be re-executed while the value is cached.
```typescript
state.putFromPromiseIfPristine(() => Promise.resolve('my new value'));
```
To find out if there is an active promise request, use `state.hasActivePromiseRequest()`. You can also explicitly clear and put from promise in one step:
```typescript
state.clearAndPutFromPromise(Promise.resolve('overridden value'));
```
You can get an RXJS observable to the value stream with `state.values$()`:
```typescript
state
.values$()
.subscribe(val => console.log("Observed value " + val));
```
You can also observe the `changes` which includes undefined values
```typescript
state
.changes$()
.subscribe(val => console.log("Observed " + (val ? "String value" : "Undefined"));
```
## MultiInputState
The `MultiInputState` is basically a map with a string key and an `InputState` as its value. It is used for most of the cache stores in OpenProject.
To create a MultiInputState, you can use the helper method `multiInput<Type>()` . To get an InputState member of this map, use the following:
```typescript
export type FooType = { id:number };
const multi = multiInput<FooType>();
const state = multi.get('my identifier');
state.putValue({ id: 1234 });
// Later on
multi.get('my identifier').value // { id: 1324}
```
The MultiInputState can be observed as a whole:
```typescript
multi
.observeChange()
.subscribe(([changedId, foo]) => {
console.log(`CHANGE for ${changedId}: ${foo?.id || 'cleared'});
}
multi.clear('my identifier');
// CHANGE for my identifier: cleared
multi
.get('my identifier')
.putFromPromiseIfPristine(() => Promise.resolve({ id: 'new' }));
// CHANGE for my identifier: new
```
## StatesGroup
The `StatesGroup` aggregates multiple States or MultiInputStates into one class. The only benefit to this is debugging capabilities of the reactivestates library. You can call the following method in development mode to see all changes to states in a StateGroup logged to console:
```typescript
window.enableReactiveStatesLogging();
```
This might then look like the following, with green color for added objects, and red color for removed values:
```
[RS] Changesets.changesets[/api/v3/projects/1] {o=4} "[object Object]"
```
## 🔗 Code references
- [`StatesService`](https://github.com/opf/openproject/blob/dev/frontend/src/app/components/states.service.ts) Global `States` cache of MultiInputStates
- [`IsolatedQuerySpace`](https://github.com/opf/openproject/blob/dev/frontend/src/app/modules/work_packages/query-space/isolated-query-space.ts) Query space `StatesGroup`. Is instantiated multiple times whenever a work package query is loaded. See [the separate concept guide](../queries) for more information.
- [ReactiveStates](https://github.com/ReactiveStates/reactivestates) library we use for the StatesGroup. This was developed by Roman primarily for us during AngularJS times.
## Discussions
- In contrast to a `Store` concept of redux, the States and state groups do not have any concept of data immutability. As a caller you will need to ensure that. In OpenProject, many of the states are in fact mutable due to historic reasons and the fact that complex class instances are passed around that cannot be easily shallow copied. This will need to be refactored in the future.
- As the reactivestates library was primarily developed for us, we may need to take over its code or move to a different state management concept altogether. The recent developments in `ngxs` look very promising.