Skip to content

Screen ViewModel

Task

Model one screen as a view model that owns screen-local state, derived values, and user actions.

Solution

Treat the screen as one feature boundary. Keep the screen state, loading flags, derived values, and actions inside a view model instead of scattering them across UI components.

Code

ts
import { declareViewModel, readonlyAtom } from '@nrgyjs/core';

export const ProfileScreenViewModel = declareViewModel(({ scope }) => {
  const tab = scope.atom<'info' | 'activity'>('info');
  const saving = scope.atom(false);

  return {
    state: {
      tab: readonlyAtom(tab),
      saving: readonlyAtom(saving),
    },
    openInfo: () => tab.set('info'),
    openActivity: () => tab.set('activity'),
    save: async () => {
      saving.set(true);

      try {
        // save screen data
      } finally {
        saving.set(false);
      }
    },
  };
});
tsx
import { useAtoms, withViewModel } from '@nrgyjs/react';

const ProfileScreen = withViewModel(ProfileScreenViewModel)(({ viewModel }) => {
  const { tab, saving } = useAtoms(viewModel.state);

  return (
    <section>
      <button onClick={viewModel.openInfo}>Info</button>
      <button onClick={viewModel.openActivity}>Activity</button>
      <div>Current tab: {tab}</div>
      <button disabled={saving} onClick={() => void viewModel.save()}>
        Save
      </button>
    </section>
  );
});

What to Watch Out For

  • the view model should represent one coherent screen flow
  • UI components should render state and forward intent, not own screen workflow
  • screen lifecycle should define when the view model is destroyed

Common Mistakes

  • splitting one screen workflow across many unrelated component states
  • calling services directly from leaf components
  • keeping screen-level effects outside the screen owner