View accompanying code on Github
As much as I enjoyed building this website with Remix, I wanted to give arguably the largest/most popular SSR React framework a shot: Next.js. So, I built a 2-page recipe aggregate that will scrape and standardize my favorite recipe sources, and then display them in a pretty grid. Oh, and because it's the year 2024, I had to include an LLM component. Tutorials are a dime a dozen, so I'm writing this less as a comprehensive deep dive, and more as contemplation.
Jack Herrington has some interesting thoughts on the progression of AI development, or rather, app development using AI. His focus is on "applied AI" or how AI is being utilized by the developer. To sum it up, the progression ladder goes as following:
In software engineering, consistency and predictability is king, which is how we came about pure functions and idempotency. AI is exactly the opposite. It's a black box where variability can be constrained, but never fully encapsulated. Even if graduating through all the steps above, if you provide a model with the same prompt a million times, you'd most likely receive a million different responses.
When thinking about how to incorporate an LLM into this project, I realized that there's no need to integrate a model into the app. The I/O in each layer of the app requires regularity. But that doesn't mean I can't employ an LLM into the development process.
Now back to the project at hand. As I mentioned above, I'm going to give a quick highlight to the how/what/why of Next.js, React 19, and developing this app. For the most part, I stuck to a core features instead of reaching outwards to libraries.
I really wanted to integrate a LLM somewhere within this somewhat contrived app, but honestly, I couldn't find a reasonable "why." Each layer of the app was designed to be pure and layering in an LLM was just adding unpredictability, which typically cascades into instability. The app went roughly as follows:
Any LLM layer would just be plain silly and expensive considering how many tokens it would require. So instead, I integrated it into my workflow. This project required a lot of busy work via DOM selectors. Essentially, I would make a simple call to the recipe source, parse out the returned HTML, and then extract each recipe node. Since the returned HTML structure is more or less the same on each subsequent API call, I prompted an AI chatbot with the said document and request the selectors in return. To build in some resiliency, I also retrieved a set of query selectors, so if one failed, we'd simply move on to the next until we hit a match.
As we further evolve towards SSR, React
server actions are way to
delineate the environment a function should be call. By using a use server
directive inside a function, we are telling React to create a reference to the
server function that's accessible to the client. In the following for the /
route, I do a very minimal amount of form validation on the server:
In this example, when a user submits the form, a network request to search
is
made, the formData
is handled, and either returns void
or performs a
redirect (in which another RSC is rendered and sent to the client).
There's a couple things to note here:
There is no use server
directive on the top level because Next.js has
designated RSC as the default options, which means client components are opt-in
only. Before being sent to the client, React will first "render" the component
once and only once. During this process, it will see that the search
function
is server-only and will declare it in the server enviro and pass a reference to
the Home
function. If we excluded the directive, then the function would be
declared within the scope of the Home
function, or in other words, it would be
treated as any other function declaration. Because I don't have any other state
items and am relying on the browser to handle the form state, I can simply pass
a single action to the form.
This render-once behavior also allows for async
components. Take the following
example:
Again, no use client
directive, so Next.js will render this once on the
server, wait until all promises are resolved, and then send to the client.
That pretty much sums it up. If you'd like to hear more, then please leave a comment below!
Comments