DEV Community

noire.munich
noire.munich

Posted on

Flex your cells

Some context

We've been building SportOffice on RedwoodJS for almost a year now and we've made a point to use the framework as it comes - with little to no exotic stuff sprayed over.

This helped us go live in December and today we are hitting numbers ( € ), with a CRM built entirely with RedwoodJS ( and, yeah, Stripe, AWS, we're in an ecosystem anyway ). RW's not in v1 yet, but there's no looking back for us.

Now with all its power in its standard setup, couple of things could need some exposition online to help people better understand what's possible with it.

Today, I will precisely talk about Cells.

Reminding you of Cells

In a standard Redwood app, you'd have a web and an api side, both self explanatory. The api would be powered by Redwood itself - but it could be anything else, really, from an Express served api to a stitched graphql schema and beyond ( sky's the limit to Redwoods ).

Cells are components that manage the whole fetch-some-data-display-in-front cycle, errors and empty payloads included. A typical Cell will at the very least be a module with no default export, exporting:

  • a const QUERY = gql[...]
  • a const Success: React.FC<SuccessProps> = [...]

Sample below.

It's clean and easy, I've been using them for so long I don't even know if it ever felt difficult. Certainly it felt great to leave fetch calls in React components behind.

So, cells in themselves are very convenient, but sometimes you need a bit more flexibility. What if you needed to call entirely different queries but the rest of the component should stay the same? It's doable with a standard cell, but not in a very clean way.

Some code

What I'm about to show you is not neat and shiny, it's unpolished code extracted to demonstrate the point - please forgive me if your eyes do bleed. This is the price of knowledge ( for some of us ).

We needed a Select for all our Users where roles.include('student'). This has been enough for about ten months:

import { userToOption } from 'src/components/Model/User'  
import { Input, Select } from 'src/ui'  

export const QUERY = gql`  
 query SELECT_STUDENT($where: WhereUserInput) {  
     options: students(where: $where) {  
         id  
         firstname
         lastname  
     }
}`  

export const Loading = () => (  
  <Input name={'student'} disabled pointer={'model:student.label'} />  
)  

export const Success: typeof Select = ({  
  name = 'students',  
  options,  
  ...selectProps  
}) => (  
  <Select  
    {...selectProps}  
    name={name}  
    pointer={'model:student.label'}  
    options={options?.map((student) => userToOption(student))}  
  />  
)
Enter fullscreen mode Exit fullscreen mode

It uses a students service with a where parameter, it's safe for you to assume that this should fit straight into a prisma query.

Problem now is that we need the same Select, targeting the same role, but in different contexts that will actually require different db queries.

One way to do that could be to pass an argument to our graphql query and then, on the api side, switch over it to trigger different method calls.
Though it's a valid way to handle this in some cases, I wasn't too keen this time on doing so. I'd prefer to keep my methods and endpoints explicit and focused, which I found to be more scalable.

To do so I created 3 endpoints, each with their own api services & separate methods, to fetch my students in their different context. And to make sure this would be used properly in front, I relied on createCell ( formerly withCell), to select the query I'd need to call:

import { createCell } from '@redwoodjs/web'  
import { userToOption } from 'src/components/Model/User'  
import { Input, Select } from 'src/ui'  

interface CellProps {  
  sessionId?: number  
  courseId?: number  
}  

export const QUERY_ALL_STUDENTS = gql`  
 query QUERY_ALL_STUDENTS($where: WhereUserInput) {  
   options: students(where: $where) {  
     id  
     firstname
     lastname  
   }
}`  

export const QUERY_SESSION_STUDENTS = gql`  
 query QUERY_SESSION_STUDENTS($id: Int) {  
   options: getSessionStudents(id: $id) {  
     id  
     firstname
     lastname  
   }
}`  

export const QUERY_COURSE_STUDENTS = gql`  
 query QUERY_COURSE_STUDENTS($id: Int) {  
   options: getCourseStudents(id: $id) {
     id  
     firstname
     lastname  
   }
}`  

const Loading = () => (  
  <Input name={'student'} disabled pointer={'model:student.label'} />  
)  

const Success = ({ selectProps, name, options }) => {  
  return (  
    <Select  
      {...selectProps}  
      name={name}  
      pointer={'model:student.label'}  
      options={options?.map((student) => userToOption(student))}  
    />  
 )  
}  

export default function ({ sessionId, courseId }: CellProps) {  
  const { query, id } = React.useMemo(() => {  
    switch (true) {  
      case Boolean(sessionId && !courseId):  
        return { id: sessionId, query: QUERY_SESSION_STUDENTS }  
      case Boolean(!sessionId && courseId):  
        return { id: courseId, query: QUERY_COURSE_STUDENTS }  
      default:  
        return { query: QUERY_ALL_STUDENTS }  
    }  
  }, [sessionId, courseId])  

  return createCell({  
    QUERY: query,  
    displayName: 'StudentsSelect',  
    Loading,  
    Success,  
  })({ id })  
}
Enter fullscreen mode Exit fullscreen mode

I think this is the cleanest way I've found so far to deal with this.

It let's me keep a very clean API - which I'm going to really need further on as this is an important part of our business, and it lets me avoid creating dozens of the same component with only one prop to differentiate them.

So at the end of the day, I feel like it's clean enough on the web and api sides of my lawn.

Cheers,


Notes


  • Notice how each query has their own name? Whichever way you want to tackle such issue, always keep in mind graphql client will require you to use query names as if they were ids. RedwoodJS will let some warning show if you don't comply.
  • Cells are documented here, here as well and here

Top comments (0)