DEV Community

loading...
Cover image for a first look at redwoodJS: part 4 - cells

a first look at redwoodJS: part 4 - cells

ajcwebdev profile image anthonyCampolo Updated on ・8 min read

What I’ve experienced and what I know many people have experienced learning React and getting into this is that path right now is very, very, very, very, very long. And hard. And horrible.

Tom Preston-Werner - Full Stack Radio

  • In part 1 we installed and created our first RedwoodJS application
  • In part 2 we created links to our different page routes and a reusable layout for our site
  • In part 3 we got our database up and running and learned to create, retrieve, update, and destroy blog posts.

In this part we'll set up our frontend to query data from our backend to render a list of our blog posts to the front page. If you've never worked with GraphQL or serverless functions like Lambda then some of the concepts in this part may be new.

4.1 api/src

We've seen the prisma folder under api, now we'll look at the src folder containing our GraphQL code. Redwood comes with GraphQL integration built in to make it easy to get our client talking to our serverless functions.

01-api-src-folder

4.2 graphql.js

The functions folder will contain any lambda functions your app needs in addition to the graphql.js file auto-generated by Redwood. This file is required to use the GraphQL API.

// api/src/functions/graphql.js

import {
  createGraphQLHandler,
  makeMergedSchema,
  makeServices,
} from '@redwoodjs/api'

import schemas from 'src/graphql/**/*.{js,ts}'
import services from 'src/services/**/*.{js,ts}'
import { db } from 'src/lib/db'

export const handler = createGraphQLHandler({
  schema: makeMergedSchema({
    schemas,
    services: makeServices({ services }),
  }),
  onException: () => {
    // Disconnect from your database with an unhandled exception.
    db.$disconnect()
  },
})
Enter fullscreen mode Exit fullscreen mode

We will be working more with the schema definition language and our services so you don't need to worry too much about what's going on in this file. See these docs for further explanation.

4.3 posts.sdl.js

graphql contains your GraphQL schema. The files will end in .sdl.js. GraphQL schemas for a service are specified using the GraphQL SDL (schema definition language) which defines the API interface for the client.

// api/src/graphql/posts.sdl.js

export const schema = gql`
  type Post { ... }
  type Query { ... }
  input CreatePostInput { ... }
  input UpdatePostInput { ... }
  type Mutation { ... }
`
Enter fullscreen mode Exit fullscreen mode

Our schema has five object types each with their own fields and types.

Post - A blog post

// api/src/graphql/posts.sdl.js

type Post {
  id: Int!
  title: String!
  body: String!
  createdAt: DateTime!
}
Enter fullscreen mode Exit fullscreen mode

Query - A query that retrieves either:

  • multiple posts in an array
  • a single post with an id
// api/src/graphql/posts.sdl.js

type Query {
  posts: [Post!]!
  post(id: Int!): Post
}
Enter fullscreen mode Exit fullscreen mode

CreatePostInput - title and body input of new post

// api/src/graphql/posts.sdl.js

input CreatePostInput {
  title: String!
  body: String!
}
Enter fullscreen mode Exit fullscreen mode

UpdatePostInput - title and body input of updated post

// api/src/graphql/posts.sdl.js

input UpdatePostInput {
  title: String
  body: String
}
Enter fullscreen mode Exit fullscreen mode

Mutation - Create, update, or delete post

  • createPost - Takes title and body from CreatePostInput and creates a Post
  • updatePost - Takes title and body from UpdatePostInput and id of the updated Post
  • deletePost - Takes id of the deleted Post
// api/src/graphql/posts.sdl.js

type Mutation {
  createPost(input: CreatePostInput!): Post!
  updatePost(id: Int!, input: UpdatePostInput!): Post!
  deletePost(id: Int!): Post!
}
Enter fullscreen mode Exit fullscreen mode

4.4 db.js

lib contains one file, db.js, which instantiates the Prisma database client. You can use this folder for other code related to the API side that doesn't fit in functions or services. Prisma Client is an auto-generated and type-safe query builder that's tailored to your data.

// api/src/lib/db.js

import { PrismaClient } from '@prisma/client'

export const db = new PrismaClient()
Enter fullscreen mode Exit fullscreen mode

4.5 posts.js

services contain business logic related to your data. A service implements the logic of talking to the third-party API. This is where your code for querying or mutating data with GraphQL ends up. The difference is it's in a format that's reusable in other places in your application.

// api/src/services/posts/posts.js

import { db } from 'src/lib/db'

export const posts = () => {
  return db.post.findMany()
}

export const post = ({ id }) => {
  return db.post.findOne({
    where: { id },
  })
}

export const createPost = ({ input }) => {
  return db.post.create({
    data: input,
  })
}

export const updatePost = ({ id, input }) => {
  return db.post.update({
    data: input,
    where: { id },
  })
}

export const deletePost = ({ id }) => {
  return db.post.delete({
    where: { id },
  })
}
Enter fullscreen mode Exit fullscreen mode

Redwood will automatically import and map resolvers from the services file onto your SDL. You write resolvers in a way that makes them easy to call as regular functions from other resolvers or services.

Redwood will look in api/src/services/posts/posts.js for these five resolvers.

4.6 redwood generate cell

Now we are going to create a cell that will render the most recent blog posts to the front page.

yarn rw g cell BlogPosts
Enter fullscreen mode Exit fullscreen mode

Redwood will generate four files:

  • BlogPostsCell.js
  • BlogPostsCell.test.js
  • BlogPostsCell.stories.js
  • BlogPostsCell.mock.js

02-generating-cell-files-BlogPosts

In BlogPostsCell.js there will be a QUERY that uses JSON.stringify to render the results of the query. But there is one thing we need to change.

// web/src/components/BlogPostsCell/BlogPostsCell.js

export const QUERY = gql`
  query {
    blogPosts {
      id
    }
  }
`

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 = ({ blogPosts }) => {
  return json.stringify(blogPosts)
}
Enter fullscreen mode Exit fullscreen mode

We need to make a slight adjustment to get our QUERY to match up with the schema that we have already created. Change each instance of blogPosts to just posts.

// web/src/components/BlogPostsCell/BlogPostsCell.js

export const QUERY = gql`
  query {
    posts {
      id
    }
  }
`

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 json.stringify(posts)
}
Enter fullscreen mode Exit fullscreen mode

Now we can take the BlogPostsCell component and insert it into our HomePage component. We need to first import it, and then place the tag inside of the BlogLayout tags.

// web/src/pages/HomePage/HomePage.js

import BlogLayout from 'src/layouts/BlogLayout'
import BlogPostsCell from 'src/components/BlogPostsCell'

const HomePage = () => {
  return (
    <BlogLayout>
      <p><a href="https://dev.to/ajcwebdev">Blog</a></p>
      <p><a href="https://twitter.com/ajcwebdev">Twitter</a></p>
      <p><a href="https://github.com/ajcwebdev">GitHub</a></p>
      <BlogPostsCell />
    </BlogLayout>
  )
}

export default HomePage
Enter fullscreen mode Exit fullscreen mode

This gives us just the id and the typename which is Post.

03-BlogPostsCell-render-HomePage

Lets go back to our QUERY and add in title, body, and createdAt.

// web/src/components/BlogPostsCell/BlogPostsCell.js

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 json.stringify(posts)
}
Enter fullscreen mode Exit fullscreen mode

Now we should get all the info we need on the home page.

04-BlogPostsCell-render-id-title-body-createdAt

This doesn't look very good though, I don't think anyone would want to read this blog. In the BlogPostsCell file inside Success we can create a component for our posts and give it a little structure.

// web/src/components/BlogPostsCell/BlogPostsCell.js

export const Success = ({ posts }) => {
  return posts.map(post => (
    <article key={post.id}>
      <header>
        <h2>{post.title}</h2>
      </header>
      <time>{post.createdAt}</time>
      <p>{post.body}</p>
    </article>
  ))
}
Enter fullscreen mode Exit fullscreen mode

We'll do some more styling later on but for now we have our posts rendered to the front page.

05-BlogPostsCell-render-map-over-posts

4.7 BlogPost page

yarn rw g page BlogPost
Enter fullscreen mode Exit fullscreen mode

06-generating-page-files-BlogPost

import { Link, routes } from '@redwoodjs/router'

const BlogPostPage = () => {
  return (
    <>
      <h1>BlogPostPage</h1>
      <p>
        Find me in <code>./web/src/pages/BlogPostPage/BlogPostPage.js</code>
      </p>
      <p>
        My default route is named <code>blogPost</code>, link to me with `
        <Link to={routes.blogPost()}>BlogPost</Link>`
      </p>
    </>
  )
}

export default BlogPostPage
Enter fullscreen mode Exit fullscreen mode
// web/src/components/BlogPostsCell/BlogPostsCell.js

import { Link, routes } from '@redwoodjs/router'

// QUERY, Loading, Empty and Failure definitions...

export const Success = ({ posts }) => {
  return posts.map((post) => (
    <article key={post.id}>
      <header>
        <h2>
          <Link to={routes.blogPost()}>{post.title}</Link>
        </h2>
      </header>
      <p>{post.body}</p>
      <div>Posted at: {post.createdAt}</div>
    </article>
  ))
}
Enter fullscreen mode Exit fullscreen mode
// web/src/Routes.js

<Route path="/blog-post/{id}" page={BlogPostPage} name="blogPost" />
Enter fullscreen mode Exit fullscreen mode
// web/src/components/BlogPostsCell/BlogPostsCell.js

<Link to={routes.blogPost({ id: post.id })}>{post.title}</Link>
Enter fullscreen mode Exit fullscreen mode

4.8 BlogPost cell

yarn rw g cell BlogPost
Enter fullscreen mode Exit fullscreen mode

07-generating-cell-files-BlogPost

export const QUERY = gql`
  query BlogPostQuery {
    blogPost {
      id
    }
  }
`

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

export const Success = ({ blogPost }) => {
  return JSON.stringify(blogPost)
}

Enter fullscreen mode Exit fullscreen mode
// web/src/pages/BlogPostPage/BlogPostPage.js

import BlogLayout from 'src/layouts/BlogLayout'
import BlogPostCell from 'src/components/BlogPostCell'

const BlogPostPage = () => {
  return (
    <BlogLayout>
      <BlogPostCell />
    </BlogLayout>
  )
}

export default BlogPostPage
Enter fullscreen mode Exit fullscreen mode
// web/src/components/BlogPostCell/BlogPostCell.js

export const QUERY = gql`
  query BlogPostQuery($id: Int!) {
    post(id: $id) {
      id
      title
      body
      createdAt
    }
  }
`

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

export const Success = ({ post }) => {
  return JSON.stringify(post)
}
Enter fullscreen mode Exit fullscreen mode
// web/src/pages/BlogPostPage/BlogPostPage.js

const BlogPostPage = ({ id }) => {
  return (
    <BlogLayout>
      <BlogPostCell id={id} />
    </BlogLayout>
  )
}
Enter fullscreen mode Exit fullscreen mode

08-blog-post-error

// web/src/Routes.js

<Route path="/blog-post/{id:Int}" page={BlogPostPage} name="blogPost" />
Enter fullscreen mode Exit fullscreen mode

09-blog-post-1

4.9 BlogPost component

yarn rw g component BlogPost
Enter fullscreen mode Exit fullscreen mode

10-generating-component-files-BlogPost

// web/src/components/BlogPost/BlogPost.js

const BlogPost = () => {
  return (
    <div>
      <h2>{'BlogPost'}</h2>
      <p>{'Find me in ./web/src/components/BlogPost/BlogPost.js'}</p>
    </div>
  )
}

export default BlogPost
Enter fullscreen mode Exit fullscreen mode
// web/src/components/BlogPost/BlogPost.js

import { Link, routes } from '@redwoodjs/router'

const BlogPost = ({ post }) => {
  return (
    <article>
      <header>
        <h2>
          <Link to={routes.blogPost({ id: post.id })}>{post.title}</Link>
        </h2>
      </header>
      <div>{post.body}</div>
    </article>
  )
}

export default BlogPost
Enter fullscreen mode Exit fullscreen mode
// web/src/components/BlogPostsCell/BlogPostsCell.js

import BlogPost from 'src/components/BlogPost'

// Loading, Empty, Failure...

export const Success = ({ posts }) => {
  return posts.map((post) => <BlogPost key={post.id} post={post} />)
}
Enter fullscreen mode Exit fullscreen mode
// web/src/components/BlogPostCell/BlogPostCell.js

import BlogPost from 'src/components/BlogPost'

// Loading, Empty, Failure...

export const Success = ({ post }) => {
  return <BlogPost post={post} />
}
Enter fullscreen mode Exit fullscreen mode

11-blog-post-1-component

12-home-page-with-blog-post-links

In the next part we'll create a contact form.

Discussion

pic
Editor guide