/* Copyright 2025 Element Creations Ltd. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE files in the repository root for full details. */ import { useEffect, useState } from "react"; import type { BaseViewModel } from "./BaseViewModel"; type VmCreator> = () => B; /** * Instantiate a view-model that gets disposed when the calling react component unmounts. * In other words, this hook ties the lifecycle of a view-model to the lifecycle of a * react component. * * @param vmCreator A function that returns a view-model instance * @returns view-model instance from vmCreator * @example * const vm = useCreateAutoDisposedViewModel(() => new FooViewModel({prop1, prop2, ...}); */ export function useCreateAutoDisposedViewModel>(vmCreator: VmCreator): B { /** * The view-model instance may be replaced by a different instance in some scenarios. * We want to be sure that whichever react component called this hook gets re-rendered * when this happens, hence the state. */ const [viewModel, setViewModel] = useState(vmCreator); /** * Our intention here is to ensure that the dispose method of the view-model gets called * when the component that uses this hook unmounts. * We can do that by combining a useEffect cleanup with an empty dependency array. */ useEffect(() => { let toDispose = viewModel; /** * Because we use react strict mode, react will run our effects twice in dev mode to make * sure that they are pure. * This presents a complication - the vm instance that we created in our state initializer * will get disposed on the first cleanup. * So we'll recreate the view-model if it's already disposed. */ if (viewModel.isDisposed) { const newViewModel = vmCreator(); // Change toDispose so that we don't end up disposing the already disposed vm. toDispose = newViewModel; setViewModel(newViewModel); } return () => { // Dispose the view-model when this component unmounts toDispose.dispose(); }; /** * We explicitly provide an empty dependency array as we don't expect the viewModel/viewCreator to * change. * Or to put it in another way, the only reason to use this hook is to create/dispose the view-model * and that is something that should only happen at the start/end of the lifecycle of this component. */ // eslint-disable-next-line react-compiler/react-compiler // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return viewModel; }