DishFetch: The Vanilla JS Recipe App That Thinks Like a Framework

A close look at how strict MVC, manual DOM diffing, and fraction-aware ingredient rendering turn a simple recipe app into a surprisingly polished client-side system.

6 to 8 min read • View on GitHub • More from VrundaBramhe

A wide editorial illustration of a kitchen counter and browser window split into two update styles. One side shows a page being torn out and replaced all at once, while the other side shows a watchmaker swapping a single gear tooth and leaving the rest of the mechanism moving.
DishFetch’s core idea is not larger scope. It is smaller, more selective updates that keep the interface feeling stable.
Key Takeaways

DishFetch is interesting for a simple reason. It gets a modern, reactive feel without importing a modern reactive framework. The surprise is not that it works. The surprise is how little machinery it needs to feel polished.

The quiet trick that makes DishFetch feel modern

The standout move lives in the shared view base class. Instead of replacing a whole recipe view every time the state changes, DishFetch generates new markup, turns it into a DOM fragment, compares it with what is already on screen, and patches only the differences. That means fewer visual resets, less flicker, and a better chance of preserving focus and scroll position.

The update loop is the repo’s signature move. It borrows the feel of a virtual DOM without asking the app to adopt a framework.

Why the MVC split matters here

MVC can sound ceremonial in a small app. Here it is practical. `model.js` owns the data and transformations, `controller.js` coordinates events and state transitions, and the view classes focus on rendering and user input. That separation is what makes the manual patching approach possible without turning the code into a pile of special cases.

ConcernFramework-heavy recipe SPADishFetch
Rendering strategyImplicit component rerendersManual fragment diff and targeted DOM updates
State ownershipOften spread across components and hooksCentral state object in the model layer
Event handlingFramework event system and component callbacksController-driven handlers wired into views
UX preservationDepends on framework behaviorUpdate method avoids unnecessary teardown
Complexity costLibrary abstraction plus app logicMore explicit code, less hidden machinery

The controller acts as the traffic cop. It responds to route changes, user actions, and async fetches, then tells the model what to load and the view what to render. That structure keeps the rendering trick from leaking into business logic.

The state machine hiding inside a recipe app

The app’s behavior is mostly state transformation. A recipe is loaded, the current servings change, bookmarks persist in local storage, and uploaded recipes are parsed into a shape the API accepts. The UI is mostly a reflection of that state, not the owner of it.

// Conceptual flow from the repository analysis
async function controlRecipes() {
  const id = window.location.hash.slice(1);
  if (!id) return;

  recipeView.renderSpinner();
  await model.loadRecipe(id);
  recipeView.render(model.state.recipe);
}

function updateServings(newServings) {
  model.updateServings(newServings);
  recipeView.update(model.state.recipe);
}

That last line matters. `update()` is the bridge between state and the selective DOM patch. The controller changes data, the view updates only what needs to change, and the rest of the interface stays put.

Fractions are a UX feature, not a math feature

Recipe apps live or die on readability. A quantity like `0.75` is technically correct and practically annoying. DishFetch uses `fraction.js` so ingredients can render as `3/4`, which is the version a human cook can parse instantly.

A close-up editorial illustration of hands sorting recipe ingredients into structured fields while a fraction wheel converts decimals into simple fractions. The scene shows precision at the scale of a single ingredient row, not a whole page.
Small readability decisions become product decisions when a recipe list has to stay legible at different serving sizes.

That choice is tiny, but it says a lot. The app is not just converting numbers. It is translating machine-friendly data into something people can actually use while cooking.

Ingredient upload is the kind of boring feature that reveals good architecture

Upload flows tend to expose the seams in an app. DishFetch handles that by treating form data as structured input, not a blob. The repository analysis shows a pattern built around `Object.fromEntries(new FormData(this))`, validation of ingredient rows, and a transformation step that turns user-entered fields into API-ready JSON.

// Conceptual upload flow based on the repository analysis
const data = Object.fromEntries(new FormData(this));
const ingredients = Object.entries(data)
  .filter(([key, value]) => key.startsWith('ingredient') && value)
  .map(([_, value]) => {
    const [quantity, unit, description] = value.split(',').map(str => str.trim());
    return { quantity: quantity ? +quantity : null, unit, description };
  });

This is the kind of code that only looks mundane if you ignore the design problem it solves. It turns a messy, variable-length user form into a clean data object without asking the UI to become a special case factory.

How DishFetch compares

DishFetch is not trying to beat a framework on abstraction count. It is trying to spend complexity where it earns its keep. Compared with a typical React-style recipe SPA, it gives up library conventions in exchange for transparency. Compared with a naive vanilla JS app, it adds just enough structure to stay calm under change.

DimensionFramework recipe appNaive vanilla JS appDishFetch
UI updatesFramework-managed rerendersFull section rerendersIncremental patching in a custom view base class
OrganizationComponent-orientedOften ad hocStrict MVC with specialized views
State persistenceUsually app-level toolingManual and inconsistentLocal storage integrated into the model
Ingredient scalingFramework feature or custom codeOften rough around the edgesHuman-friendly fractions via `fraction.js`
Upload flowForm libraries or component stateDirect DOM parsingStructured parsing and validation

That middle column is the trap DishFetch avoids. Pure vanilla can stay simple only until the app becomes real. After that, discipline matters more than syntax.

What DishFetch suggests about modern vanilla JS

DishFetch reads like a case study in selective borrowing. It takes the useful ideas from frameworks, such as unidirectional state flow, isolated views, and incremental updates, then implements them with native browser APIs. The result is not a framework clone. It is a compact argument that careful architecture can make a small app feel much larger than it is.

That is the real lesson here. Vanilla JavaScript is not the limitation. Haphazard structure is. DishFetch shows that if the state is centralized, the rendering is incremental, and the user-facing details are humane, the app can feel surprisingly mature without becoming heavy.