# Getting Started import { Tab, Tabs } from 'fumadocs-ui/components/tabs' import { Step, Steps } from 'fumadocs-ui/components/steps' ## Installation Run the following command to add React MVVM and all needed libraries: npm pnpm yarn bun ```bash npm install react-mvvm mobx mobx-react-lite reflect-metadata ``` ```bash pnpm add react-mvvm mobx mobx-react-lite reflect-metadata ``` ```bash yarn add react-mvvm mobx mobx-react-lite reflect-metadata ``` ```bash bun add react-mvvm mobx mobx-react-lite reflect-metadata ``` ## Preparation You must import `reflect-metadata` in your main script file so that you can use the decorators. You can also configure this package, but this step is optional. Import reflect-metadata in your main entry file: ```tsx import 'reflect-metadata'; // [!code focus] import { configure } from 'react-mvvm'; import { createRoot } from 'react-dom/client'; // Optional step configure({ // Configuration options }); createRoot(document.getElementById('root')!).render(); ``` You can use `mobx` with versions **4, 5 or 6**. And it's recommended to use the 6th one. In case you want to use versions 4 or 5 you should add the following code to your webpack configuration: ```javascript module.exports = { // ... // [!code focus:5] ignoreWarnings: [ { module: /react-mvvm/, }, ], // ... }; ``` ## Usage You can find examples of using this package in [the examples section](/docs/examples). ## Further reading Despite the fact that React MVVM is an extremely small library, it can greatly affect the development process. For a better understanding of the beauty of this library, we advise you to read an [article about MobX and MVVM](https://dev.to/yoskutik/mobx-with-mvvm-makes-frontend-developers-life-much-more-easier-than-redux-does-547j). # Changelog ### Changed \[!toc] * Fields `parent` and `viewProps` now are not `undefined` during the first view render. ### Changed \[!toc] * Fields `parent` and `viewProps` now are not `undefined` in the constructor of view model; * Due to the fact above `parent` is no more observable; * Field `parent` now can be typed as `null` in case there's no parent view model; * Returned functions of `view` and `childView` now have 2 generics - for typing props and * forwarded ref. Now, it is better to use these generics rather explicitly typing props via `FC`: ```tsx // Before const View: FC = view(SomeViewModel)(() =>
) // After const View = view(SomeViewModel)(() =>
) ``` * The README file is now consist of a minimal example. The documentation can be found on the * [website](https://beautyfree.github.io/react-mvvm/). ### Removed \[!toc] * Type `ViewWithRef` was removed. To type forwarded ref of view or child view you can now use * the second generic of these functions: ```tsx // Before const View: ViewWithRef = view(SomeViewModel)(() => forwardRef(() =>
) ) // After const View = view(SomeViewModel)(() => forwardRef(() =>
) ) ``` * Removed an idle render of view components for initializing `parent` and `viewProps` fields. ### Changed \[!toc] * The size of the packages was reduces from 1.8Kb to 1.6Kb # Changelog ### Changed \[!toc] * Fields `parent` and `viewProps` now are not `undefined` during the first view render. ### Changed \[!toc] * Fields `parent` and `viewProps` now are not `undefined` in the constructor of view model; * Due to the fact above `parent` is no more observable; * Field `parent` now can be typed as `null` in case there's no parent view model; * Returned functions of `view` and `childView` now have 2 generics - for typing props and * forwarded ref. Now, it is better to use these generics rather explicitly typing props via `FC`: ```tsx // Before const View: FC = view(SomeViewModel)(() => (
)); // After const View = view(SomeViewModel)(() => (
)); ``` * The README file is now consist of a minimal example. The documentation can be found on the * [website](https://beautyfree.github.io/react-mvvm/). ### Removed \[!toc] * Type `ViewWithRef` was removed. To type forwarded ref of view or child view you can now use * the second generic of these functions: ```tsx // Before const View: ViewWithRef = view(SomeViewModel)(() => ( forwardRef(() =>
) )); // After const View = view(SomeViewModel)(() => ( forwardRef(() =>
) )); ``` * Removed an idle render of view components for initializing `parent` and `viewProps` fields. ### Changed \[!toc] * The size of the packages was reduces from 1.8Kb to 1.6Kb # ChildView import { Tabs, Tab } from 'fumadocs-ui/components/tabs' ## Overview As the `view`, the `childView` function creates an object that also implements the view logic from the MVVM pattern. But there's a big difference in these functions - ChildView does not call `vmFactory` and uses a view's view model as own one. By default, every child view is an observer and is memoized. And you can change it. The options of creating view child is same as for view. ## Usage ```tsx childView()(Component[, options]) ``` See using ChildView: [Example](/docs/examples/basic-examples#using-childview). ### Options See setting options: [Example](/docs/examples/basic-examples#setting-options). ## Usage Example ```tsx import React from 'react' // [!code focus] import { childView } from 'react-mvvm' import type { SomeViewModel } from './path-to-view-model' export type Props = { prop1: number prop2?: string } // [!code focus] export const SomeChildView = childView()( ( { viewModel, prop1, prop2 } // [!code focus] ) => (
{viewModel.field1} // [!code focus] {prop1} {prop2}
) ) ``` *** ## ChildViewComponent We highly recommend using React MVVM with functional style components. But to increase compatibility, we added a class `ChildViewComponent`, so you can create instances of ChildView as class components. If you want to create an instance of View as class component, please see the [example](/docs/examples/basic-examples#using-class-components). The only difference between class-style ChildView and functional-style ChildView is that in the class-style `viewModel` field is part of class, while in the function-style it's a property. And since functional-style child views are declared with `memo`, `ChildViewComponent` is extended from `PureComponent`. ### Usage Example ```tsx import React from 'react' // [!code focus] import { ChildViewComponent } from 'react-mvvm' import { SomeViewModel } from './path-to-view-model' export type Props = { prop1: number prop2?: string } // [!code focus] class SomeChildView extends ChildViewComponent { render() { return ( // viewModel is a class member, not a property of the component
{this.viewModel.field1}
// [!code focus] ) } } ``` # Configuration ## Overview This library can be configured at the global level. ## Usage ```tsx configure({ vmFactory, Wrapper }) ``` #### `vmFactory` This function tells the view how to create an instance of a view model. By default, all view models are simply creating with a `new` keyword. See configuring vmFactory: [Example](/docs/examples/basic-examples#configuring-vmfactory). #### `Wrapper` A wrapper which is used in `view` and `childView`. By default, `Wrapper` is equal to `React.Fragment`. The wrapper is useful if you want to add an ErrorBoundary or for a debugging purposes. See configuring the wrapper: [Basic usage](/docs/examples/basic-examples#configuring-wrapper), [Error Boundary](/docs/examples/useful-examples#using-error-boundary). ## Usage sample ```tsx import { FC, ReactElement } from 'react' // [!code focus] import { configure } from 'react-mvvm' const CustomWrapper: FC<{ children: ReactElement }> = ({ children }) => { // do anything you want return (
You can also add JSX {children}
) } // [!code focus:8] configure({ vmFactory: (VM) => { const viewModel = new VM() // do anything you want return viewModel }, Wrapper: CustomWrapper, }) ``` # Core Concepts import { Cards, Card } from 'fumadocs-ui/components/card' ## Overview React MVVM implements the MVVM (Model-View-ViewModel) pattern for React applications. The main components are: Creates components that display data and handle user interactions Reuses parent ViewModel without creating new instances Stores observable fields and logic for updating them Global configuration for ViewModel factories and wrappers # View import { Tabs, Tab } from 'fumadocs-ui/components/tabs' ## Overview The `view` function creates an object that implements the view logic from the MVVM pattern. This means that view should be responsible for displaying a component in your application. A view takes a `vmFactory` from the `configuration` and call it to create an instance of a view model. Also view is responsible for updating view model's fields `parent` and `viewProps` and for calling view's lifecycle hooks in the instance of a view model. By default, every view is an observer. But you can configure it. One of the issues that solves this package is the purity of the code. The fewer props your components have, the easier it will be to understand your code for others. And with this package you can minimize amount of props passed by observing view model's fields, its parent's fields and so on. For example, a ChildView can observe its View props. **Every view is memoized**. And as it states below, the fewer props your view having, the faster your application will work. Since the view uses `memo` function, you can also pass the `propsAreEqual` function to it. ## Usage ```tsx view(SomeViewModel)(Component[, options]) ``` ### Options There are two options: `observer` and `propsAreEqual`. If `observer` is `false`, when view will be created as non-observer component. And if `propsAreEqual` is set, it will be passed to a `memo` function (See [how memo works](https://reactjs.org/docs/react-api.html#reactmemo) for better understanding). See setting view options: [Example](/docs/examples/basic-examples#setting-options). ## Usage Example ```tsx import React from 'react' import { view } from 'react-mvvm' // [!code focus] import { SomeViewModel } from './path-to-view-model' export type Props = { prop1: number prop2?: string } // [!code focus] export const SomeView = view(SomeViewModel)( ( { viewModel, prop1, prop2 } // [!code focus] ) => (
{viewModel.field1} // [!code focus] {prop1} {prop2}
) ) ``` # ViewModel import { Cards, Card } from 'fumadocs-ui/components/card' ## Overview The `ViewModel` class is an object that implements the view model logic from the MVVM pattern. In general case, the `ViewModel` is designed to store observable fields, as well as logic for updating them. The `ViewModel` stores a reference to the props object with which the view was rendered with and also a reference to the parent view model. Also, `ViewModel` has a few view's lifecycle methods. ## What is `parent` for a ViewModel? The assignment of the parent view model occurs according to the virtual DOM tree. If `View2` is located somewhere inside `View1`, then `ViewModel1` will be considered the parent of `ViewModel2`. ## Properties #### `@observable.ref readonly parent` A link to a parent view model. See typing and using parent view model: [Example](/docs/examples/basic-examples#typing-parent-and-viewprops). #### `@observable.ref readonly viewProps` A link to a props the view has rendered with. Every time the view is renders it updates this field. Every view is memoized, and this mean that this object will be updated only if at least 1 property has been changed. Be careful observing `viewProps`. If some of yours observer components or reactions are using `viewProps`, they might update every time any prop has changed, even if the updated prop is not used directly. For better understanding of how you should observe the props, please, see the example. See typing, using and observing viewProps: [Example](/docs/examples/basic-examples#observing-viewprops). ## Methods ### Lifecycle Methods #### `protected onViewMounted?()` A hook which is called after the view becomes mounted. The function is called in the `useEffect` hook. #### `protected onViewUpdated?()` A hook which is called after the view is rendered besides the first render. This function is called in the `useEffect` hook. #### `protected onViewUnmounted?()` A hook which is called after the view becomes unmounted. The function is called in the `useEffect` hook. #### `protected onViewMountedSync?()` A hook which is called after the view becomes mounted. The function is called in the `useLayoutEffect` hook. #### `protected onViewUpdatedSync?()` A hook which is called after the view is rendered besides the first render. This function is called in the `useLayoutEffect` hook. #### `protected onViewUnmountedSync?()` A hook which is called after the view becomes unmounted. The function is called in the `useLayoutEffect` hook. See using view hooks: [Example](/docs/examples/basic-examples#view-lifecycle-hooks). ### Reactions and Disposers #### `protected autorun(...args)` An add-on function for an `autorun` from MobX. When view becomes unmounted, the disposer of this function will be called automatically. #### `protected reaction(...args)` An add-on function for a `reaction` from MobX. When view becomes unmounted, the disposer of this function will be called automatically. #### `protected addDisposer(disposer)` A function which adds a disposer that will be called after the view becomes unmounted. MobX states that **you should always dispose of reactions**. This is why `autorun`, `reaction` and `addDisposer` were added to a `ViewModel`. So, please, use it every time you want to create reactions *inside a view model*. Otherwise, you can create a memory leak. See observing: [Example](/docs/examples/basic-examples#creating-reactions). ## Usage Example ```tsx // [!code focus] import { ViewModel } from 'react-mvvm' import { action, observable, makeObservable } from 'mobx' import type { ParentViewModel } from '../path-to-parent-view-model' import type { Props } from './path-to-view' // [!code focus] export class SomeViewModel extends ViewModel { @observable field1 = 0 @observable field2 = 'field' constructor() { super() makeObservable(this) } // [!code focus] protected onViewMounted() { // do something } @action doSomething = () => { // do something } } ``` # Basic Examples import { Cards, Card } from 'fumadocs-ui/components/card' import { Tabs, Tab } from 'fumadocs-ui/components/tabs' ## Basic Examples This section contains examples of basic usage of entities with all possible typings and variants. Learn how to create and configure views and child views Understand ViewModel lifecycle, reactions, and parent-child relationships Configure ViewModel factories and wrappers globally ## View and ChildView The interfaces of `view` and `childView` are pretty much the same. The only difference - the way the view model is typed. ### Using `childView` The component which was created with `childView` must be used somewhere inside a view of the same view model. ```tsx import React from 'react' // [!code focus] import { view, childView } from 'react-mvvm' import { SomeViewModel } from './path-to-view-model' // [!code focus] export const ChildView = childView()(({ viewModel }) =>
) // [!code focus:3] // ChildView does not create a view model and should be located somewhere inside a view. // Thus, it can use view's view model. export const View1 = view(SomeViewModel)(({ viewModel }) => ) // [!code focus:2] // It doesn't have to be the direct child export const View2 = view(SomeViewModel)(({ viewModel }) => (
// [!code focus]
)) ``` ### Typing props By default, `view` and `childView` returns an `FC` component with no props. But you can type it using `FC` type. ```tsx import React from 'react' // [!code focus] import { view, childView } from 'react-mvvm' import { SomeViewModel } from './path-to-view-model' // [!code focus:3] // View1 and ChildView1 don't have any props export const View1 = view(SomeViewModel)(({ viewModel }) =>
) export const ChildView1 = childView()(({ viewModel }) =>
) // [!code focus:4] export type Props = { prop1: number // A required prop prop2?: string // An optional prop } // [!code focus:3] export const View2 = view(SomeViewModel)( ({ viewModel, prop1, prop2 }) =>
) // [!code focus:3] export const ChildView = childView()( ({ viewModel, prop1, prop2 }) =>
) // [!code focus:3] // And now you can pass the props // // ``` ### Setting options By default, `view` and `childView` create a memoized observer component. You can make it non-observer or pass `propsAreEqual` function to the `memo` HOC. ```tsx import React from 'react' // [!code focus] import { view, childView } from 'react-mvvm' import { SomeViewModel } from './path-to-view-model' // [!code focus:3] // View1 and ChildView1 are observers and they are memoized export const View1 = view(SomeViewModel)(({ viewModel }) =>
) export const ChildView1 = childView()(({ viewModel }) =>
) // [!code focus:4] // View2 and ChildView2 are not observers now, but they still memoized with default behaviour export const View2 = view(SomeViewModel)(({ viewModel }) =>
, { observer: false, }) // [!code focus] export const ChildView2 = childView()( ({ viewModel }) =>
, // [!code focus] { observer: false } ) type Props = { prop1: number } // [!code focus:3] const propsAreEqual = (prevProps: Props, nextProps: Props) => { // logic here } // [!code focus:4] // And this is how you can change propsAreEqual function for the memo export const View3 = view(SomeViewModel)(({ viewModel }) =>
, { propsAreEqual, }) // [!code focus] export const ChildView3 = childView()( ({ viewModel }) =>
, // [!code focus] { propsAreEqual } ) ``` ### Using `forwardRef` Of course, there's an opportunity to pass a ref via `view` and `childView`. You just need to apply `forwardRef` before applying these functions. Also, if you want to type the component, you have to use second generic. ```tsx import React, { forwardRef } from 'react' // [!code focus] import { view, childView } from 'react-mvvm' import { SomeViewModel } from './path-to-view-model' // [!code focus:4] // Only ref with no props export const View1 = view(SomeViewModel)( forwardRef(({ viewModel }, ref) =>
) ) // [!code focus:3] export const ChildView1 = childView()( forwardRef(({ viewModel }, ref) =>
) ) type Props = { prop1: number } // [!code focus:4] // With props export const View2 = view(SomeViewModel)( forwardRef(({ viewModel, prop1 }, ref) =>
) ) // [!code focus:3] export const ChildView2 = childView()( forwardRef(({ viewModel, prop1 }, ref) =>
) ) ``` ### Using class components We do not recommend writing new code with class-style components. However, we give you the opportunity to use the MVVM pattern for class components as well. A class component can't be a view, only a ChildView. However, you can additionally use the `view` function to wrap your ChildView to make it act as view. ```tsx // [!code focus] import { ChildViewComponent, view } from 'react-mvvm' import { SomeViewModel } from './SomeViewModel' type Props = { prop1: number } // [!code focus:2] // This is a child view. It should be used somewhere inside a view with SomeViewModel view model export class SomeChildView extends ChildViewComponent { render() { // [!code focus] return
{this.viewModel.field}
} } // [!code focus:3] // But if you want to make it act as view, you can wrap it with the view function export default view(SomeViewModel)((props) => ( )) ``` ## ViewModel ### Typing `parent` and `viewProps` View models have link to their parents and also have link to view's props. And you can type both of these fields. ```tsx // [!code focus] import { ViewModel } from 'react-mvvm' import type { ParentViewModel } from './ParentViewModel' import type { Props } from './path-to-view-props' // [!code focus:2] // No typings. parent is unknown, viewProps is unknown export class SomeViewModel1 extends ViewModel {} // [!code focus:2] // Parent is ParentViewModel, viewProps is unknown export class SomeViewModel2 extends ViewModel {} // [!code focus:2] // Parent is ParentViewModel, viewProps is Props export class SomeViewModel3 extends ViewModel {} // [!code focus:2] // Parent is unknown, viewProps is Props export class SomeViewModel4 extends ViewModel {} ``` ### Using `parent` If a view is located somewhere inside another view, inner view can use outer one's view model. ```tsx // [!code focus] import { view, ViewModel } from 'react-mvvm' // [!code focus:2] class ViewModel1 extends ViewModel { doSomething = () => {} } // [!code focus:5] class ViewModel2 extends ViewModel { onClick = () => { this.parent.doSomething() } } // [!code focus:5] // View2 must be located somewhere inside View1. Thus, view model of View1 will be a parent view model for View2 const View2 = view(ViewModel2)(({ viewModel }) => (