RedwoodJS ❤️ #Hacktoberfest

RedwoodJS

v0.20.0

# Cells

Cells are a declarative approach to data fetching and one of Redwood's signature modes of abstraction. In a way, Cells create space: by providing conventions around data fetching, Redwood can get in between the request and the response and perform optimizations, all without you ever having to change your code.

While it might seem like there must be lot of magic involved, a Cell is actually just a higher-order component that executes a GraphQL query and manages its lifecycle. All the logic's actually in just one file: withCell.tsx. The idea is that, by exporting named constants that match the parameters of withCell, Redwood can assemble this higher-order component out of these constants at build-time using a babel plugin!

All of this without writing a line of imperative code. Just say what is supposed to happen when, and Redwood will take care of the rest.

# Generating a Cell

You can generate a Cell with:

yarn rw generate cell <name>

This creates the directory named <name>Cell in web/src/components with four files:

~/redwood-app$ yarn rw generate cell user
yarn run v1.22.4
$ /redwood-app/node_modules/.bin/rw g cell user
  ✔ Generating cell files...
    ✔ Writing `./web/src/components/UserCell/UserCell.mock.js`...
    ✔ Writing `./web/src/components/UserCell/UserCell.stories.js`...
    ✔ Writing `./web/src/components/UserCell/UserCell.test.js`...
    ✔ Writing `./web/src/components/UserCell/UserCell.js`...
Done in 1.07s.

We'll go over each of these files in detail. But know that the file appended with just .js (in the example above, UserCell.js) contains all your Cell's logic.

Off the bat, this file exports five constants: QUERY, Loading , Empty , Failure and Success. The root query in QUERY is the same as <name> so that, if you're generating a cell based on a model in your schema.prisma, you can get something out of the database right away. But there's a good chance you won't generate your Cell this way, so if you need to, make sure to change the root query. See the Cells section of the Tutorial for a great example of this.

# Usage

With Cells, you have a total of seven exports to work with:

Name Type Description
QUERY string|function The query to execute
beforeQuery function Lifecycle hook; prepares variables and options for the query
afterQuery function Lifecycle hook; sanitizes data returned from the query
Loading component If the request is in flight, render this component
Empty component If there's no data (null or []), render this component
Failure component If something went wrong, render this component
Success component If the data has loaded, render this component

Only QUERY and Success are required. If you don't export Empty, empty results are sent to Success, and if you don't export Failure, error is output to the console.

Loading, Empty, Failure, and Success all have access to the same set of props, with Failure and Success getting exclusive access to error and data respectively. So, in addition to displaying the right component, a Cell funnels the right props to the right component.

This prop set is composed of 1) what's returned from Apollo Client's Query component, which is quite a few things—see their API reference for the full list (note that, as we just mentioned, error and data are only available to Failure and Success respectively. And Cells use loading to decide when to show Loading, so you don't get that one either)—and 2) props passed down from the parent component in good ol' React fashion.

# QUERY

QUERY can be a string or a function. Note that it's totally more than ok to have more than one root query. Here's an example of that:

export const QUERY = gql`{
  query {
    posts {
      id
      title
    }
    authors {      id      name    }  }
}

So in this case, both posts and authors would be available to Success:

export const Success = ({ posts, authors }) => {
  // render logic with posts and authors
}

If QUERY is a function, it has to return a valid GraphQL syntax tree. Use a function if your queries need to be more dynamic:

But what about variables? Well, Cells are setup to use any props they receive from their parent as variables (things are setup this way in beforeQuery). For example, here BlogPostCell takes a prop, numberToShow, so numberToShow is just available to your QUERY:

import BlogPostsCell from 'src/components/BlogPostsCell'

const HomePage = () => {
  return (
    <div>
      <h1>Home</h1>
      <BlogPostsCell numberToShow={3} />    </div>
  )
}

export default HomePage
export const QUERY = gql`
  query($numberToShow: Int!) {    posts(numberToShow: $numberToShow) {      id
      title
    }
  }
`

This means you can think backwards about your Cell's props from your SDL: whatever the variables in your SDL are, that's what your Cell's props should be.

# beforeQuery

beforeQuery is a lifecycle hook. The best way to think about it is as an API for configuring Apollo Client's Query component (so you might want to check out the docs for it).

By default, beforeQuery gives any props passed from the parent component to Query so that they're available as variables for QUERY. It'll also set the fetch policy to 'cache-and-network' since we felt that this is the behavior users want most of the time.

export const beforeQuery = (props) => {
  return { variables: props, fetchPolicy: 'network-only' }
}

But you can of course override this by exporting your own.

# afterQuery

afterQuery is a lifecycle hook. It runs just before data gets to Success. Use it to sanitize data returned from QUERY before it gets there.

By default, afterQuery just returns the data as it is:

export const afterQuery = (data) => ({...data})

# Loading

If the request is in flight, a Cell renders Loading.

For a production example, navigate to predictcovid.com, the first site made with Redwood. Usually, when you first navigate there, you'll see most of the dashboard spinning. Those are Loading components in action!

When you're developing locally, you can catch your Cell waiting to hear back for a moment if set your speed in the Inspector's Network tab to something like "Slow 3G".

But why bother with Slow 3G when Redwood comes with Storybook? Storybook makes developing components like Loading (and Failure) a breeze. We don't have to put up with hacky workarounds like Slow 3G or intentionally breaking our app just to develop our components.

# Empty

A Cell renders this component if there's no data.

What do we mean by no data? We mean if the response is 1) null or 2) an empty array ([]). There's actually four functions in withCell.js dedicated just to figuring this out:

// withCell.js

const isDataNull = (data) => {
  return dataField(data) === null
}

const isDataEmptyArray = (data) => {
  return Array.isArray(dataField(data)) && dataField(data).length === 0
}

const dataField = (data) => {
  return data[Object.keys(data)[0]]
}

const isEmpty = (data) => {
  return isDataNull(data) || isDataEmptyArray(data)
}

# Failure

A Cell renders this component if something went wrong. You can quickly see this in action (it's easy to break things) if you add a nonsense field to your QUERY:

const QUERY = gql`
  query {
    posts {
      id
      title
      nonsense    }
  }
`

But, like Loading, Storybook is probably a better place to develop this.

# Success

If everything went well, a Cell renders Success.

As mentioned, Success gets exclusive access to the data prop. But if you try to destructure it from props, you'll notice that it doesn't exist. This is because Redwood adds another layer of convenience: in withCell.js, Redwood spreads data (using the spread operator, ...) into Success so that you can just destructure whatever data you were expecting from your QUERY directly.

So, if you're querying for posts and authors, instead of doing:

export const Success = ({ data }) => {
  const { posts, authors } = data

  // render logic with posts and authors
  ...
}

Redwood does:

export const Success = ({ posts, authors }) => {
  // render logic with posts and authors
  ...
}

Note that you can still pass any other props to Success. After all, it's still just a React component.

# When should I use a Cell?

A good rule of thumb for when to use a Cell is if your component needs some data from a database or other service that may be delayed in responding. Let Redwood worry about juggling what is displayed when. You just focus on what those things should look like.

For one-off queries, there's always useApolloClient. This hook returns the client, which you can use to make queries:

client = useApolloClient()
client.query({
  query: gql`
    ...
  `
})

# Can I Perform a Mutation in a Cell?

Absolutely. We do so in our example todo app. We also don't think it's an anti-pattern to do so. Far from it—your cells might end up containing a lot of logic and really serve as the hub of your app in many ways.

It's also important to remember that, besides exporting certain things with certain names, there aren't many rules around Cells—everything you can do in a regular component still goes.

# How Does Redwood Know a Cell is a Cell?

You just have to end a filename in "Cell" right? Well, while that's basically correct, there is one other thing you should know.

Redwood looks for all files ending in "Cell" (so if you want your component to be a Cell, its filename does have to end in "Cell"), but if the file 1) doesn't export a const named QUERY and 2) has a default export, then it'll be skipped.

When would you want to do this? If you just want a file to end in "Cell" for some reason. Otherwise, don't worry about it!

# Advanced Example: Implementing a Cell Yourself

If we didn't do all that built-time stuff for you, how might you go about implementing a Cell yourself?

Consider the example from the Tutorial where we're fetching posts:

export const QUERY = gql`
  query {
    posts {
      id
      title
      body
      createdAt
    }
  }
`

export const Loading = () => <div>Loading...</div>

export const Empty = () => <div>No posts yet!</div>

export const Failure = ({ error }) => (
  <div>Error loading posts: {error.message}</div>
)

export const Success = ({ posts }) => {
  return posts.map((post) => (
    <article>
      <h2>{post.title}</h2>
      <div>{post.body}</div>
    </article>
  ))
}

And now let's say that Babel isn't going to come along and assemble our exports into a higher-order component. What might we do?

We'd probably do something like this:

const QUERY = gql`
  query {
    posts {
      id
      title
      body
      createdAt
    }
  }
`

const Loading = () => <div>Loading...</div>

const Empty = () => <div>No posts yet!</div>

const Failure = ({ error }) => (
  <div>Error loading posts: {error.message}</div>
)

const Success = ({ posts }) => {
  return posts.map((post) => (
    <article>
      <h2>{post.title}</h2>
      <div>{post.body}</div>
    </article>
  ))
}

const isEmpty = (data) => {
  return isDataNull(data) || isDataEmptyArray(data)
}

export const Cell = () => {
  return (
    <Query query={QUERY}>
      {({ error, loading, data }) => {
        if (error) {
          if (Failure) {
            return <Failure error={error} />
          } else {
            console.error(error)
          }
        } else if (loading) {
          return <Loading />
        } else if (data) {
          if (typeof Empty !== 'undefined' && isEmpty(data)) {
            return <Empty />
          } else {
            return <Success {...data} />
          }
        } else {
          throw 'Cannot render Cell: graphQL success but `data` is null'
        }
      }}
    </Query>
  )
}

That's a lot of code. A lot of imperative code too.

We're basically just dumping the contents of withCell.js into this file. Can you imagine having to do this every time you wanted to fetch data that might be delayed in responding? Yikes.