Skip to content

useViewModel

Andrei Fangli edited this page Sep 14, 2023 · 2 revisions
API / useViewModel<TViewModel> hook

Watches a view model for property changes.

function useViewModel<TViewModel extends INotifyPropertiesChanged>(
    viewModel: TViewModel,
    watchedProperties?: readonly (keyof TViewModel)[]
): void

Template Parameters

  • TViewModel: the type of view model to watch.

Parameters

  • viewModel: TViewModel, the view model to watch.
  • watchedProperties: readonly Array<keyof TViewModel>, optional, when provided, a render will be requested when only one of these properties has changed.

Creates a new instance of a view model of the given type and watches for property changes, constructor arguments act as dependencies.

function useViewModel<TViewModel extends INotifyPropertiesChanged, TConstructorArgs extends readonly any[]>(
    viewModelType: ViewModelType<TViewModel, TConstructorArgs>,
    constructorArgs?: ConstructorParameters<ViewModelType<TViewModel, TConstructorArgs>>,
    watchedProperties?: readonly (keyof TViewModel)[]
): TViewModel

Template Parameters

  • TViewModel: the type of view model to initialize.
  • TConstructorArgs the constructor parameter types.

Parameters

  • viewModelType: ViewModelType<TViewModel, TConstructorArgs>, the type object (class declaration or expression) of the view model.
  • constructorArg: _TConstructorArgs, the constructor arguments used for initialization, whenever these change a new instance is created.
  • watchedProperties: readonly Array<keyof TViewModel>, optional, when provided, a render will be requested when only one of these properties has changed.

Returns: TViewModel

Returns the initialized view model instance.


Examples

For instance, view models that perform API calls usually require a constructor parameter for an object that can perform this task. This helps with testing as well since we would be able to provide a mock or a stub and mimic the API call.

In cases like this, the useViewModel overload will allow us pass the class object and afterwards provide the constructor arguments. If any of these arguments change a new instance is created and returned by the hook.

This is sometimes necessary as we want to ensure a clean state when a parameter changes, such as the ID of the entity we are currently seeing. All other hooks can be dependent on the entity ID as well which will ensure that the state is reinitialized from scrath even when the component does not unmount.

class MyViewModel extends ViewModel {
    private _myEntity: IMyEntity | null;
    private _isLoading: false;
    private readonly _entityId: string;
    private readonly _apiService: IApiService;

    public constructor(entityId: string, apiService: IApiService) {
        super();

        this._entityId = entityId;
        this._apiService = apiService;
    }

    public get isLoading(): boolean {
        return this._isLoading;
    }

    public get myEntity(): IMyEntity | null {
        return this._myEntity;
    }

    public async loadAsync(): Promise<void> {
        this._isLoading = true;
        this.notifyPropertiesChanged("isLoading");

        try {
            this._myEntity = await this._apiService.fetchMyEntity(this._entityId);
            this.notifyPropertiesChanged("myEntity");
        }
        finally {
            this._isLoading = false;
            this.notifyPropertiesChanged("isLoading");
        }
    }
}

interface IMyComponentProps {
    readonly entityId: string;
}

function MyComponent({ entityId }: IMyComponentProps): JSX.Element {
    // Can use React Context for dependency injection
    const apiService = useApiService();
    const viewModel = useViewModel(MyViewModel, [entityId, apiService]);

    // Load the entity whenever the view model is a different instance.
    useEffect(() => { viewModel.loadAsync(); }, [viewModel]);

    return (
        <>
            {viewModel.isLoading ? "loading" : null}
            <EntityDisplay entity={viewModel.myEntity} />
        </>
    );
}

There may be cases when we do not want to create a new instance whenever constructor arguments change. In this case we can use useViewModelMemo<TViewModel>. This is a very simple hook that creates an instance using useMemo and watches the result view model for changes.

For instance, if we know we get a new instance of the API service whenever we call the related function we can remove this from the deps. This would also imply that we are not dealing with a custom hook when retrieving this service otherwise its state would be handled inside the function.

function MyComponent({ entityId }: IMyComponentProps): JSX.Element {
    const viewModel = useViewModelMemo(
        () => {
            const apiService = getApiService();

            return new MyViewModel(entityId, apiService);
        },
        [entityId]
    );

    // This still works, we get a new instance
    // only when the entity id changes.
    useEffect(() => { viewModel.loadAsync(); }, [viewModel]);

    return (
        <>
            {viewModel.isLoading ? "loading" : null}
            <EntityDisplay entity={viewModel.myEntity} />
        </>
    );
}
Clone this wiki locally