Meal app

This app was built to be a meal planning assistant. It keeps an inventory of your food, a list of your own recipes, and your grocery purchases. This project has been a great way for me to try out new technologies and has gone through several iterations over the years.

screenshot

Created with

  • React
  • GraphQL
  • Apollo Client
  • Styled-components
  • PostgreSQL
  • Apollo Server/Go with gqlgen*
* I originally built the backend in Node and Apollo Server, and later built a second version of the server in Go as a way to get started learning the language. I used the gqlgen library to maintain the GraphQL functionality and stay compatible with my Apollo-based frontend.

Cool features

A list of your own recipes

All your meals and their ingredients live in a library of dishes. You can sort by the last used date, which is helpful in keeping a rotation. To that end, you can tag dishes that you want to be active in your rotation, so if you have meals that you don’t want as often, you can filter them out. All dishes can be filtered by your own categories or tags .

Advanced filtering

The categories can be filtered by matching any of the selected categories, or all of them (i.e. an OR filter or an AND filter).

In sync with your inventory

For each dish, you can see which ingredients you have and don’t have.

Add purchased items

When entering in purchase items, the form remembers details about the current item that you’ve previously entered and autofills them for you.

Price averages

An item’s details page shows you its average price based on your purchase history, as well as the average price for each location you purchased it at. This helps you stay informed about your cost efficiency.

Use items efficiently

The inventory helps you reduce your food waste by sorting by expiration dates and listing dishes they can be used in.

The ingredients in dishes have a great deal of flexibility. You can make things optional, list substitutes, and specify another ingredient that this item “counts as” (e.g. your sandwich dish has “cheese” as an ingredient, but you have “cheddar cheese” in your fridge; for this situation you can make cheddar cheese fulfill the requirement for “cheese”). Substitutes like that are taken into account when filtering dishes on whether or not you have all the required ingredients.

ingredient options

Additionally, you can use other dishes as ingredients. This is helpful for things like homemade sauces that go into different things, or other meals that are assembled, like sandwiches or burrito bowls. It was essential to include this feature in order to avoid messy duplicates across dishes and ingredients and to keep the data more true to its source.

These functionalities drastically changed the original database design and added a significant amount of complexity to the business logic, but greatly increased the overall usefulness of the system.

Background and goal

I used to be terrible at figuring out what to cook on the spot. I’d end up in a last-minute scramble every single day, and it was a huge source of stress for me. I became obsessed with avoiding that stress, and dove deep into meal planning. I planned out every meal as far in advance as I could and made sure I had maximum variety from day to day. I ended up constantly buying ingredients for every dish under the sun, and I couldn’t keep track of them and use them up in time. Things went bad, and I’d kick myself for it.

The obvious solution to me was, “I need a database of every food item in my house.” And almost immediately I started having big dreams of how everything could fit into a system that could tell me anything I wanted to know. What can I make that doesn't use chicken, and isn't a soup, that I haven't had for a while? What do I need to use out of the fridge ASAP, and what can I make with it? Have I been spending too much on eggs when I could be buying them cheaper elsewhere? More important than all of these questions was how I could do more with less. I was spending so much time and energy on perfecting my process and I wanted everything to be automated. I wanted to be able to switch my brain off and, ideally, create something that could do it all better than I could.

Technical challenges

In a couple of places in the app you have a big list of items or dishes that you can manipulate in different ways: you can sort it by different fields, you can apply filters, and you can do a search. You want these all to be applied at once; for instance if you have a filter active, you want to be able to search within the filtered results. It was a really tricky thing to implement, particularly with handling state and the filtered copy of the original data. When I tried to do multiple things with the list, running different sorts and filters on this state variable, I very quickly got into infinite rerenders. There’s probably a much better way to solve this, but I ended up using a single useEffect that runs on every search and every filter change and sorting change, and checks against each of them.

Another challenge I ran into was handling dates. Using gqlgen, your models are generated from your schema and they’re strongly typed. So dates coming back from the database as a date type must be a string in the resolvers, and you need to decide how you want that string formatted, whether it’s the number of milliseconds or something else. I messed around with changing the data type in different places, and each caused its own set of problems. In the end I modified my queries to cast the dates to text, so the conversion is made as early as possible. This has worked out for me so far, but I'll be keeping it in mind during future changes that might cause data loss or other problems.

Spotlight: <datalist> element

When you're inputting an ingredient in a recipe, or logging the purchase of an item, you want that item to be from the single source of items that already exist. You don't want to have a bunch of names that mean the same thing. "cheese", "cheddar cheese", "cheese, cheddar". That sort of thing. You want to essentially pick from a list. But when your list of every item ever gets in the hundreds, you really don't want to have to scroll through a dropdown. A <datalist> was an ideal solution for this problem because you can just type in what you're looking for and see what already exists.

The other key benefit is that if you need to add a new item, you're not restricted to the available options, the way you would be with a <select>. You can enter in the new name as with a regular input, and the app will add that item to the database along with whatever dish or purchase item you're creating.