DEV Community

Cover image for React + MVVM: How I'm Organizing My Frontend for Scalability
luis-botelho
luis-botelho

Posted on

React + MVVM: How I'm Organizing My Frontend for Scalability

The Hook

Have you ever felt like your React components are doing too much? πŸ˜… Fetching data, managing loading/error state, formatting it, AND rendering the UI β€” all in one file.

In my project, SafeAnchor (a vertical SaaS for maritime fleet management), I decided to move away from "vibe coding" and commit to a proper architecture: MVVM (Model-View-ViewModel).

![SafeAnchor MVVM data flow: View, ViewModel, Service and API]

Why MVVM?

For a junior developer in 2026, understanding system architecture β€” not just syntax β€” is a must-have skill. Here's how I decoupled my frontend logic into three layers, straight from apps/frontend/src/ in the repo:

src/
β”œβ”€β”€ views/         # screens (JSX)
β”œβ”€β”€ viewmodels/    # screen state + logic (custom hooks)
β”œβ”€β”€ services/      # API communication
└── models/        # data shape
Enter fullscreen mode Exit fullscreen mode

1. Services β€” pure API logic

Services don't care about buttons or spinners. They only care about talking to the API and shaping the response. This is the actual getVesselById from services/vesselService.js:

export async function getVesselById(id) {
  try {
    const response = await fetch(`${apiUrl}/vessels/${id}`);
    if (!response.ok) {
      throw new Error("Nao foi possivel carregar a embarcaΓ§Γ£o.");
    }
    const data = await response.json();
    return { vessel: data, source: "api", error: false };
  } catch (error) {
    const vessel = fallbackVessels.find((v) => v.id === Number(id));
    return { vessel, source: "fallback", error: false };
  }
}
Enter fullscreen mode Exit fullscreen mode

Look closely at the catch: if the request fails, the service returns a hardcoded local vessel instead of throwing. That's a deliberate resilience decision β€” more on it below.

2. Hooks as ViewModels β€” the brain

All state management β€” loading, fetching, error handling β€” lives here. This is what keeps the UI clean. From viewmodels/useVesselDetailsViewModel.js:

export function useVesselDetailsViewModel(id) {
  const [vessel, setVessel] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState("");

  useEffect(() => {
    async function loadVessel() {
      try {
        setIsLoading(true);
        const loaded = await getVesselById(id);
        setVessel(loaded.vessel);
      } catch (error) {
        setError("NΓ£o foi possΓ­vel carregar a embarcaΓ§Γ£o.");
      } finally {
        setIsLoading(false);
      }
    }
    loadVessel();
  }, [id]);

  return { vessel, isLoading, error, removeVessel };
}
Enter fullscreen mode Exit fullscreen mode

3. Clean Views β€” just props in, JSX out

views/VesselDetailsPage.jsx only receives data via the hook. No fetching, no logic β€” which makes it much faster to test and maintain:

export default function VesselDetailsPage() {
  const { id } = useParams();
  const viewModel = useVesselDetailsViewModel(id);

  if (viewModel.isLoading) return <p>Carregando...</p>;
  if (viewModel.error) return <p>{viewModel.error}</p>;

  const vessel = viewModel.vessel;

  return (
    <main className="vessel-details">
      <h1 className="vessel-details__title">EmbarcaΓ§Γ£o</h1>
      <span className="vessel-details__value">{vessel.name}</span>
      {/* ... */}
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

Put together, the data flows one way: API β†’ Service β†’ ViewModel (Hook) β†’ View (JSX) β€” no shortcuts, no components secretly reaching into the network layer.

The Fallback That Made Me Rethink "MVP Code"

![Resilience fallback flow when the backend is offline]

The nicest surprise re-reading my own code: vesselService.js already falls back to an in-memory vessel array when the Express backend is unreachable. The ViewModel and the View don't know (or care) which path ran β€” they just get { vessel, source, error }. One try/catch buys the whole app the ability to stay demoable even with the backend down.

Where SafeAnchor Actually Stands

  • βœ… Epic 001 β€” Fleet Management Foundation: vessel listing, details, creation, editing, archiving β€” all shipped.
  • πŸ”œ Epic 002 β€” Maintenance Management: up next.
  • πŸ—ΊοΈ Later: Safety Checklists (with QR-code field access), Document Management, Maritime Academy.

My Tech Stack & Learning Process

Right now it's React + Vite on the frontend and Express on the backend, with a documented target migration to Next.js + TypeScript + Redux Toolkit as the app grows into a feature-based structure. To speed up my learning, I use NotebookLM to organize scattered concepts into structured technical guides β€” basically an AI-powered tutor for engineering fundamentals.

What's Next?

  • Refactoring CSS to the BEM standard
  • Hardening the fallback/error paths ahead of Maintenance Management
  • Building out Safety Checklists and QR-code field access

Let's Discuss! πŸ‘‡

How do you handle state logic in your React apps? Do you prefer big all-in-one hooks, or a decentralized architecture like MVVM? Let me know in the comments!

SafeAnchor #WebDev #React #Architecture #LearningInPublic #SoftwareEngineering

Top comments (0)