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.
- DishFetch feels modern because its custom `update()` method patches only what changed instead of repainting the whole screen.
- The app’s MVC split is not decorative. It keeps state, rendering, and event handling separate enough to support that incremental update loop.
- Fraction handling and ingredient uploads are small features that reveal the real design goal: make messy recipe data behave like a clean product.
- The project argues that vanilla JavaScript can still produce a framework-like experience when rendering and state management are disciplined.
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.
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.
| Concern | Framework-heavy recipe SPA | DishFetch |
|---|---|---|
| Rendering strategy | Implicit component rerenders | Manual fragment diff and targeted DOM updates |
| State ownership | Often spread across components and hooks | Central state object in the model layer |
| Event handling | Framework event system and component callbacks | Controller-driven handlers wired into views |
| UX preservation | Depends on framework behavior | Update method avoids unnecessary teardown |
| Complexity cost | Library abstraction plus app logic | More 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.
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.
| Dimension | Framework recipe app | Naive vanilla JS app | DishFetch |
|---|---|---|---|
| UI updates | Framework-managed rerenders | Full section rerenders | Incremental patching in a custom view base class |
| Organization | Component-oriented | Often ad hoc | Strict MVC with specialized views |
| State persistence | Usually app-level tooling | Manual and inconsistent | Local storage integrated into the model |
| Ingredient scaling | Framework feature or custom code | Often rough around the edges | Human-friendly fractions via `fraction.js` |
| Upload flow | Form libraries or component state | Direct DOM parsing | Structured 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.