Cross-Section Of A Redwood App

I'm in love with RedwoodJS. It puts opinions around all the decisions you need to make to build a React app, and abstractions around the most common features you need to make the app usable. While I might not agree with all of Redwood's opinions, I'm perfectly happy to accept them for the sake of shipping things. It allows me to focus on building features instead of worrying about infrastructure and setup.

I recently started building a training journal app with Redwood. I've been embracing pull requests as a means to tell stories on this personal project, and I noticed that some of my PRs show a nice cross-section of all the Redwood abstractions from the UI to the database.

In this article I'll give you a little background on the app I'm building and some fundamental Redwood concepts, and then we'll take a look at one of my recent PRs to see the cross-section of a Redwood app.

Some background about my app

For the last few years I've tracked my running/triathlon training plans with a spreadsheet. It's worked fine, but are we even web developers if we're not turning spreadsheets into web apps?

I'm starting to ramp up my training for this year, and I decided I want an app for tracking it this time. Nothing too complicated — I want to track my scheduled workouts, my actual workouts, and get a sense for whether I'm on-target with my training.

Here's what it looks like so far:

My training journal app, showing two weeks worth of workouts

The change I want to introduce

My database model has an entity that represents each workout I want to get in. I call it a PlanWorkout.

So far a PlanWorkout tracks things like activity (running, biking, etc.), target miles and/or duration, and intention for the workout (recovery, race, etc.).

One thing that's missing — is the workout a key workout? Is it the race I'm working up to? Is it maybe not a race, but a workout I'm really interested in crushing, like an FTP test? I like to see these workouts on my grid, so I know when they're coming up.

The PR we'll look at adds a single field — isKeyWorkout — to the PlanWorkout model, and propagates it up the entire stack, from database to UI.

Some Redwood fundamentals

Here are some things to know about Redwood:

  1. A Redwood app consists of a React front-end and a GraphQL API back-end. You don't need to set any of this up...Redwood's CLI takes care of it for you.
  2. The Redwood Page construct represents a page that a user can hit in your React app.
  3. When you want to interact with your GraphQL back-end from your React front-end, you render a Cell component.
  4. Within each Cell, you specify a few things:
    • The queries/mutations it will send to the GraphQL endpoint.
    • The Empty, Loading, Error, and Success states of the component, based on data loaded from the GraphQL endpoint.
  5. The back-end is supported by a database schema definition file, GraphQL schema definition files, and services for handling data interaction and business logic. Redwood ties these all together to give you a GraphQL endpoint with access to your database.

I've been thinking of these abstractions like a diagram of Earth's core, with the database at the center and the UI on the outside:

Cross-section of a Redwood app, in the style of a 3d globe with a cutout to show the layers inside.

A PR that introduces the changes

I've done my best to tell a story in the PR associated with this work. To summarize what's happening:

  1. We add a new field to the table representing a workout in the schema file, and run prisma's migration tool to update the local database with the new field. (Prisma is the ORM that Redwood uses under the covers.)
  2. We expose the new field in all GraphQL endpoints that need it (queries and mutations).
  3. We add the new field to all queries/mutations that need it, via the corresponding Cells.
  4. We add the new field in any UI components that will interact with it.

With these changes we can identify key workouts! Notice how the race at the end of week 2 is now (subtly) highlighted:

My training journal app, showing two weeks worth of workouts, with a race highlighted

There's not a lot to this PR, but it demonstrates changes at most of the layers in our cross-section.

A couple layers are missing. There are no changes to a Page or service — the PR is adding a new field to an existing page, and no changes are needed there. If you're curious, here is the EditPlanWorkoutPage that wraps the EditPlanWorkoutCell modified in the PR, and here is the planWorkouts service that facilitates database interactions for the PlanWorkoutForm modified in the PR.

In creating this PR, I used a similar one as a guide; when I need to add a field in the future, I'll use this PR as a guide.

Consistent patterns mean less thinking

Consider apps you've worked with that don't have consistent patterns for common problems. You've probably spent a good amount of time digging through existing code to find a pattern that solves a problem, then more time deciding which of the multiple patterns you find is most representative of the preferred approach.

With Redwood imposing opinions and structure, changes that travel the entire stack from UI to database are consistent and repeatable. No need to research the patterns and choose the correct one — there's only one pattern, and it's consistently applied throughout the app.

(And if you've worked with me in the past you just did a spit take, having watched me use semicolons on one line and no semicolons on the next, completely incapable of consistency in code.)