mirror of
https://github.com/element-hq/element-web.git
synced 2026-01-11 19:56:47 +00:00
Add more MVVM documentation (#31680)
* Move benefits section to the top * Improve existing doc * Add more documentation
This commit is contained in:
parent
540d71f49c
commit
7d72775af9
1 changed files with 139 additions and 10 deletions
149
docs/MVVM.md
149
docs/MVVM.md
|
|
@ -8,7 +8,13 @@ General description of the pattern can be found [here](https://en.wikipedia.org/
|
|||
|
||||
If you do MVVM right, your view should be dumb i.e it gets data from the view model and merely displays it.
|
||||
|
||||
### Practical guidelines for MVVM in element-web
|
||||
## Why are we using MVVM?
|
||||
|
||||
1. MVVM forces a separation of concern i.e we will no longer have large react components that have a lot of state and rendering code mixed together. This improves code readability and makes it easier to introduce changes.
|
||||
2. Introduces the possibility of code reuse. You can reuse an old view model with a new view or vice versa.
|
||||
3. Adding to the point above, in future you could import element-web view models to your project and supply your own views thus creating something similar to the [hydrogen sdk](https://github.com/element-hq/hydrogen-web/blob/master/doc/SDK.md).
|
||||
|
||||
## Practical guidelines for MVVM in element-web
|
||||
|
||||
A first documentation and implementation of MVVM was done in [MVVM-v1.md](MVVM-v1.md). This v1 version is now deprecated and this document describes the current implementation.
|
||||
|
||||
|
|
@ -19,12 +25,12 @@ This is anywhere your data or business logic comes from. If your view model is a
|
|||
#### View
|
||||
|
||||
1. Located in [`shared-components`](https://github.com/element-hq/element-web/tree/develop/packages/shared-components). Develop it in storybook!
|
||||
2. Views are simple react components (eg: `FooView`).
|
||||
3. Views use [useSyncExternalStore](https://react.dev/reference/react/useSyncExternalStore) internally where the view model is the external store.
|
||||
2. Views are simple react components (eg: `FooView`) with very little state and logic.
|
||||
3. Views must call `useViewModel` hook with the corresponding view model passed in as argument. This allows the view to re-render when something has changed in the view model. This entire mechanism is powered by [useSyncExternalStore](https://react.dev/reference/react/useSyncExternalStore).
|
||||
4. Views should define the interface of the view model they expect:
|
||||
|
||||
```tsx
|
||||
// Snapshot is the return type of your view model
|
||||
// Snapshot is the data that your view-model provides which is rendered by the view.
|
||||
interface FooViewSnapshot {
|
||||
value: string;
|
||||
}
|
||||
|
|
@ -34,16 +40,16 @@ This is anywhere your data or business logic comes from. If your view model is a
|
|||
doSomething: () => void;
|
||||
}
|
||||
|
||||
// ViewModel is a type defining the methods needed for `useSyncExternalStore`
|
||||
// ViewModel is an object (usually a class) that implements both the interfaces listed above.
|
||||
// https://github.com/element-hq/element-web/blob/develop/packages/shared-components/src/ViewModel.ts
|
||||
type FooViewModel = ViewModel<FooViewSnapshot> & FooViewActions;
|
||||
|
||||
interface FooViewProps {
|
||||
// Ideally the view only depends on the view model i.e you don't expect any other props here.
|
||||
vm: FooViewModel;
|
||||
}
|
||||
|
||||
function FooView({ vm }: FooViewProps) {
|
||||
// useViewModel is a helper function that uses useSyncExternalStore under the hood
|
||||
const { value } = useViewModel(vm);
|
||||
return (
|
||||
<button type="button" onClick={() => vm.doSomething()}>
|
||||
|
|
@ -82,8 +88,131 @@ This is anywhere your data or business logic comes from. If your view model is a
|
|||
|
||||
4. A full example is available [here](https://github.com/element-hq/element-web/blob/develop/src/viewmodels/audio/AudioPlayerViewModel.ts)
|
||||
|
||||
### Benefits
|
||||
### `useViewModel` hook
|
||||
|
||||
1. MVVM forces a separation of concern i.e we will no longer have large react components that have a lot of state and rendering code mixed together. This improves code readability and makes it easier to introduce changes.
|
||||
2. Introduces the possibility of code reuse. You can reuse an old view model with a new view or vice versa.
|
||||
3. Adding to the point above, in future you could import element-web view models to your project and supply your own views thus creating something similar to the [hydrogen sdk](https://github.com/element-hq/hydrogen-web/blob/master/doc/SDK.md).
|
||||
Your view must call this hook with the view-model as argument. Think of this as your view subscribing to the view model.<br>
|
||||
This hook returns the snapshot from your view-model.
|
||||
|
||||
## Disposables and helper hooks
|
||||
|
||||
Disposables provide a mechanism for tracking and releases resources. This is necessary for avoiding memory leaks.
|
||||
|
||||
### Lifecycle of a view model
|
||||
|
||||
The lifecycle of a given view model is from its creation (usually through the constructor i.e `new FooViewModel(prop1, prop2)`) to the time the `dispose` method on it is called (eg: `fooViewModel.dispose()`). It is the responsibility of whoever creates the view model to call the dispose method when the view model is no longer necessary.
|
||||
|
||||
Disposable work by tracking anything that needs to be disposed of and then sequentially disposing them when `viewModel.dispose()` is called.
|
||||
|
||||
### How to use disposables
|
||||
|
||||
Consider the following scenarios:
|
||||
|
||||
#### Scenario 1: Your view model listens to some event on an event emitter <br>
|
||||
|
||||
In the example given below, how do you ensure that the listener on `props.emitter` is removed when the view model is disposed?
|
||||
|
||||
```ts
|
||||
class FooViewModel ... {
|
||||
constructor(props: Props) {
|
||||
...
|
||||
props.emitter.on("my-event", this.doSomething());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can use disposables to remove the listener when the view-model is disposed:
|
||||
|
||||
```ts
|
||||
class FooViewModel ... {
|
||||
constructor(props: Props) {
|
||||
...
|
||||
this.disposables.trackListener(props.emitter, "my-event", this.doSomething());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Scenario 2: Your view model creates sub view models <br>
|
||||
|
||||
```ts
|
||||
class FooViewModel ... {
|
||||
constructor(props: Props) {
|
||||
...
|
||||
this.barViewModel = new BarViewModel(...);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Here, we want to ensure that when `FooViewModel.dispose()` is called, it also disposes any sub view models (in this case `BarViewModel`):
|
||||
|
||||
```ts
|
||||
class FooViewModel ... {
|
||||
constructor(props: Props) {
|
||||
...
|
||||
this.barViewModel = this.disposables.track(new BarViewModel(...));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Scenario 3: Tracking and disposing arbitrary resources <br>
|
||||
|
||||
A disposable is:
|
||||
|
||||
- a function
|
||||
- an object with `dispose` method (like a view model)
|
||||
|
||||
You can therefore use disposables to track any resource that must be eventually relinquished, eg:
|
||||
|
||||
```ts
|
||||
class Call {
|
||||
....
|
||||
public endCall();
|
||||
public stopConnections();
|
||||
}
|
||||
|
||||
class CallViewModel {
|
||||
...
|
||||
constructor(props: Props) {
|
||||
const call = new Call();
|
||||
// When the view model is disposed, the following call methods will be called
|
||||
this.disposables.track(() => {
|
||||
call.endCall();
|
||||
call.stopConnections();
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Disposing view models from non-MVVMed react components
|
||||
|
||||
While we eventually want all our UI code to use MVVM, the current reality is that most of the existing code is just normal react components. We follow a bottoms up approach when it comes to moving code over to MVVM i.e we deal with child components before dealing with parent components.
|
||||
|
||||
This means that you need to dispose child view models from the non-MVVMed parent component.
|
||||
|
||||
#### Class component:
|
||||
|
||||
Create the view model in `componentDidMount()` and dispose the view model in `componentWillUnmount()`:
|
||||
|
||||
```ts
|
||||
class FooComponent extends Component {
|
||||
componentDidMount() {
|
||||
this.barViewModel = new BarViewModel(...);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.barViewModel.dispose();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Functional Component:
|
||||
|
||||
Use the `useCreateAutoDisposedViewModel` hook:
|
||||
|
||||
```ts
|
||||
export function FooComponent(props) {
|
||||
const vm = useCreateAutoDisposedViewModel(() => new BarViewModel(...));
|
||||
return <BarView vm={vm}>;
|
||||
}
|
||||
```
|
||||
|
||||
This hook will call the `dispose` method on the view model when `FooComponent` is unmounted.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue