Closed58

RedwoodJS Tutorial をなぞる

lemonadernlemonadern

Chapter 1

lemonadernlemonadern

生成

yarn create redwood-app --ts ./my--redwoodblog
cd ./my--redwoodblog
yarn rw dev

http://localhost:8910/
でデフォルトページが見れる。こんなの書くか迷ったけどいちおうメモしておく

lemonadernlemonadern

デフォルトポートは 8, 9, 10!!! で覚えてね、とのこと
特に意味はないっぽい

lemonadernlemonadern

.nvmrc とか.editorconfig.vscode/ とかも生成されていてびっくりした、そんなのまで生成するんか

lemonadernlemonadern
  • メインになるのは api/, scripts/, web/ のディレクトリで、このうち api/ (バックエンド) と web/ (フロントエンド) は yarn workspace で分けられている
  • RedwoodJSでは、こいつらを sides と呼ぶらしい

workspace で分けられているので、もちろんパッケージ追加時はワークスペース指定が必要

yarn workspace web add marked
  • scripts/ はNodeランタイムで動かすスクリプトを置く場所
    デフォルトでは、DBのシードをおこなうseed.ts が置いてある
lemonadernlemonadern

api/

  • db/ は Prisma のスキーマ・マイグレーションファイルの場所
  • src/
    • directives/ : GraphQL の Schema Directive
    • functions/ : 自動生成されるもの以外だと、lambda functions を置く
    • graphql/ : GQL のスキーマ定義ファイルを置く
    • lib/ : 名前の通り
    • services/ : ビジネスロジックを書くファイルを置く
lemonadernlemonadern

web/

こちらはだいたい名前そのまま

web/src/
├── App.tsx
├── components/
├── index.css
├── index.html
├── layouts/
├── pages/
└── Routes.tsx
lemonadernlemonadern

ページをつくる

Scaffolding

yarn redwood generate page home /

すると👇

web/src/pages/HomePage/
├── HomePage.stories.tsx
├── HomePage.test.tsx
└── HomePage.tsx

が作られ、 Routes.tsx も更新される

lemonadernlemonadern

[解決済]TSのエラー出ちゃう問題

If you look in Routes you'll notice that we're referencing a component, HomePage, that isn't imported anywhere. Redwood automatically imports all pages in the Routes file since we're going to need to reference them all anyway. It saves a potentially huge import declaration from cluttering up the routes file.
https://redwoodjs.com/docs/tutorial/chapter1/first-page

Routes.tsx は デフォルトでページをインポートするのでインポートは必要無いとのことだが、TSでやると普通に Cannot Find name になる。

yarn rw type-check とかは当たり前に動くので、エディタ上のチェックだけに引っかかっている感じ。これを回避する設定があるだろうと思ったが、見つからなかった...

調べる

https://github.com/redwoodjs/redwood/issues/234#issuecomment-620906084
なにやら Auto Imports on Routes.js のところにごちゃごちゃ書いているけど、画一的な解決策を提示してくれているわけではない

似たようなIssueもなさそうな感じだった

あと、コンポーネントをimportしないせいで出てくるTSコンパイラのエラーを消す方法がわからなくて困っています。。
https://zenn.dev/link/comments/261aa5fdc9dc57

おなじとこで困ってる人いてうける

どうしよう

明示的にimportしても動くのだけど、

// 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage

みたいな挙動があるので、こちら側で上書きするのはちょっとやめておきたい

https://github.com/redwoodjs/redwood/issues/234#issuecomment-620906084
↑に挙げられているSolutionの一つの「RouteはJSファイルにする」を一旦は採用しようかな

ちゃんとして解決策はチュートリアルやってから考えます

解決してしまった

違うことやっててVSCode再起動したらエラー消えちゃった
生成されたコードの情報をLSP(?)が読めてなかったのが原因ぽい
他にページ作ったときもすぐエラーが消えたので、読み込みさせれば解決

lemonadernlemonadern
yarn redwood generate page about

これは、

yarn rw g page about

と置き換えられる

パスを指定しない場合は、ページの名前がパスに利用される

lemonadernlemonadern
yarn rw g layout blog

すると、BlogLayoutが作られる

childrenにReactNodeを受け取るLayoutコンポーネントを書いて、Routes.tsx

      <Set wrap={BlogLayout}>
        <Route path="/about" page={AboutPage} name="about" />
        <Route path="/" page={HomePage} name="home" />
      </Set>

とすると、Layout が適用される。楽すぎ

lemonadernlemonadern

途中で気づいたけど、何も設定してないのに format on save も効いている

lemonadernlemonadern
lemonadernlemonadern

Prisma Schema

Prismaを使ってDB Schemaを定義していく。ここらへんはRedwood というよりPrismaの解説

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  body      String
  createdAt DateTime @default(now())
}

migrateする

yarn rw migrate dev

Prisma Studioも使える

yarn rw prisma studio

http://localhost:5555/ で見られる

lemonadernlemonadern

scaffold すると、

yarn rw g scaffold post
  ✔ Successfully wrote file `./web/src/components/Post/EditPostCell/EditPostCell.tsx`
  ✔ Successfully wrote file `./web/src/components/Post/Post/Post.tsx`
  ✔ Successfully wrote file `./web/src/components/Post/PostCell/PostCell.tsx`
  ✔ Successfully wrote file `./web/src/components/Post/PostForm/PostForm.tsx`
  ✔ Successfully wrote file `./web/src/components/Post/Posts/Posts.tsx`
  ✔ Successfully wrote file `./web/src/components/Post/PostsCell/PostsCell.tsx`
  ✔ Successfully wrote file `./web/src/components/Post/NewPost/NewPost.tsx`
  ✔ Successfully wrote file `./api/src/graphql/posts.sdl.ts`
  ✔ Successfully wrote file `./api/src/services/posts/posts.ts`
  ✔ Successfully wrote file `./api/src/services/posts/posts.scenarios.ts`
  ✔ Successfully wrote file `./api/src/services/posts/posts.test.ts`
  ✔ Successfully wrote file `./web/src/scaffold.css`
  ✔ Successfully wrote file `./web/src/lib/formatters.tsx`
  ✔ Successfully wrote file `./web/src/lib/formatters.test.tsx`
  ✔ Successfully wrote file `./web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx`
  ✔ Successfully wrote file `./web/src/pages/Post/EditPostPage/EditPostPage.tsx`
  ✔ Successfully wrote file `./web/src/pages/Post/PostPage/PostPage.tsx`
  ✔ Successfully wrote file `./web/src/pages/Post/PostsPage/PostsPage.tsx`
  ✔ Successfully wrote file `./web/src/pages/Post/NewPostPage/NewPostPage.tsx`

yarn rw dev すると、CRUDできるUIができてる、、、俺の書いたコードはスキーマ定義だけなのに

lemonadernlemonadern

Cell のクエリに型エラーが表示される場合があるときは

  • VSCodeのリロード
  • yarn rw g types で型定義生成

のどちらかをすれば治るよ、とのこと

lemonadernlemonadern

Cell

RedwoodJS では、非同期処理が必要なページの構成に Cell と呼ばれる仕組みを使う
以下はScaffoldされたPostCellの例

PostCell.tsx
import type { FindPostById } from 'types/graphql'

import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web'

import Post from 'src/components/Post/Post'

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

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

export const Empty = () => <div>Post not found</div>

export const Failure = ({ error }: CellFailureProps) => (
  <div className="rw-cell-error">{error?.message}</div>
)

export const Success = ({ post }: CellSuccessProps<FindPostById>) => {
  return <Post post={post} />
}

PostCellは、PostPageで呼ばれているだけ

PostPage.tsx
import PostCell from 'src/components/Post/PostCell'

type PostPageProps = {
  id: number
}

const PostPage = ({ id }: PostPageProps) => {
  return <PostCell id={id} />
}

export default PostPage

これだけで表示される

lemonadernlemonadern

Cell とは

Cell は、事前に定められた名前を持つ定数の名前付きエクスポートをすると、いい感じに Redwood 側でそれを利用してくれる機能
QUERY, Loading, Empty, Failure, Success を書くと、勝手にそれが使われる

lemonadernlemonadern

Cell をつくる

もちろんCellもScaffoldできる

yarn rw g cell Articles
  ✔ Successfully wrote file `./web/src/components/ArticlesCell/ArticlesCell.mock.ts`
  ✔ Successfully wrote file `./web/src/components/ArticlesCell/ArticlesCell.test.tsx`
  ✔ Successfully wrote file `./web/src/components/ArticlesCell/ArticlesCell.stories.tsx`
  ✔ Successfully wrote file `./web/src/components/ArticlesCell/ArticlesCell.tsx`

Scaffold した場合、Cell側で生成されるクエリはCellと同名のクエリが存在すると仮定した形になる
この場合、生成されたarticleへのクエリは不正なので、post に変更する

必要なPageでimport + 配置すれば表示される。簡単すぎ

lemonadernlemonadern
query ArticlesQuery {
    articles: posts {
      id
      title
      body
      createdAt
    }
  }

クエリの値に別名を付けると、それをSccessのところで利用可能

export const Success = ({ articles }: CellSuccessProps<ArticlesQuery>) => { /* ... */ }
lemonadernlemonadern

Service

services/** ディレクトリに リゾルバを書くと、それがSDLにマップされて利用可能になる。
ビジネスロジックはここに書く

リゾルバを書かなければただの関数として扱えるので、柔軟性はありそう

lemonadernlemonadern

記事ページをつくる

yarn rw g page Article

ArticlePage が作られる

パスパラメタを利用したルーティングは以下のように定義できる

<Route path="/article/{id}" page={ArticlePage} name="article" />

ArticleCell をつくる

yarn rw g cell Article

Cellのクエリを直す
これをPageに配置する(id を渡す)

と、記事詳細の完成

パスパラメタに受け取る文字列の型も定義できる

<Route path="/article/{id:Int}" page={ArticlePage} name="article" />

こうすると、{id} は 整数しか受け取らない

lemonadernlemonadern

ArticleCell でAPIの返り値をそのまま表示している部分を、Article コンポーネントで置き換える
単なるコンポーネントもscaffoldできる

yarn rw g component Article
  ✔ Successfully wrote file `./web/src/components/Article/Article.test.tsx`
  ✔ Successfully wrote file `./web/src/components/Article/Article.stories.tsx`
  ✔ Successfully wrote file `./web/src/components/Article/Article.tsx`
lemonadernlemonadern

lint, format on Save してくれるのは嬉しいけど、 unused import を自動で消すやつも入れてほしい

lemonadernlemonadern
lemonadernlemonadern

From をつくる

  • RedwoodJS 自身が Form 関連の機能も提供している
ContactPage.tsx
import {
  Form,
  Submit,
  SubmitHandler,
  TextAreaField,
  TextField,
} from '@redwoodjs/forms'
import { MetaTags } from '@redwoodjs/web'

type FormValues = {
  name: string
  email: string
  message: string
}

const ContactPage = () => {
  const onSubmit: SubmitHandler<FormValues> = (data) => {
    console.log(data)
  }

  return (
    <>
      <MetaTags title="Contact" description="Contact page" />

      <Form onSubmit={onSubmit}>
        <label htmlFor="name">Name</label>
        <TextField name="name" />

        <label htmlFor="email">Email</label>
        <TextField name="email" />

        <label htmlFor="message">Message</label>
        <TextAreaField name="message" />
        <Submit>Submit</Submit>
      </Form>
    </>
  )
}

export default ContactPage

プレーンな見た目で、Redwoodの機能を備えたコンポーネントを標準で利用できる

lemonadernlemonadern

そういえば、このチュートリアルだと型定義に interface を使うほうに寄せてるみたい

lemonadernlemonadern

React Hook Form に似てるな〜と思ってたけど、RHFをラップして作っているっぽい

Redwood's forms are built on top of React Hook Form so there is even more functionality available than we've documented here. Visit the Form docs to learn more about all form functionalities.

lemonadernlemonadern

Formから送られてきたデータを保存する

  1. スキーマ定義
  2. migrate
  3. yarn rw g sdl Contact で、スキーマからSDL(Schema Definition Language)生成
    ⇨ Redwoodが勝手にResolverにmappingする
lemonadernlemonadern

Redwood assumes your code won't try to set a value on any field named id or createdAt so it left those out of the Input types, but if your database allowed either of those to be set manually you can update CreateContactInput or UpdateContactInput and add them.

idcreatedAt はユーザの入力で設定されないことを想定しているため、自動生成のDSLにはこれらが含まれていない

lemonadernlemonadern

scaffold した後に生成されたファイルを開くとしばしば赤線エラーが出るけど、例によってエディタをリロードすれば正しく読み込まれて解決する

lemonadernlemonadern

there are no explicit resolvers defined in the SDL file. Redwood follows a simple naming convention: each field listed in the Query and Mutation types in the sdl file (api/src/graphql/contacts.sdl.ts) maps to a function with the same name in the services file (api/src/services/contacts/contacts.ts).

SDLでは明示的にリゾルバへのマッピングを行わないが、Query および Mutation のフィールドは service ディレクトリの同名の関数にマッピングされる

lemonadernlemonadern

読み取りのみのSDLを生成する場合(CRUDが必要ない場合)

yarn rw g sdl Contact --no-crud
lemonadernlemonadern
  • Query および Mutation の操作に認証が必要ない場合、@requireAuth@SkipAuth にすればいい
  • 生成されたSDLに必要ないものがあれば、消去する
lemonadernlemonadern

コンポーネントから呼ぶ方法

  1. Mutation を 定数で定義
  2. useMutation hook の引数に、1 で定義したものを渡す
  3. hook の返り値のfunctionを onSubmit で呼び出して使う
lemonadernlemonadern

サーバーサイドバリデーション

services/contacts/contact.ts
// ...
  validate(input.email, 'email', { email: true })
// ...

validate(チェック対象, フォームのname属性, バリデーションディレクティブを含むオブジェクト)
当たり前だが、GraphQLを挟んでいる時点でプロパティの存在は保証できている(DSLでそのように定義しているなら)

サーバーエラーをフォームにフィードバックする

ContactPage.tsx
/* ... */
      <Form onSubmit={onSubmit} config={{ mode: 'onBlur' }} error={error}>
        <FormError error={error} wrapperClassName="form-error" /> 
{/* ... */}
lemonadernlemonadern
validateWith(() => {
  const oneWeekAgo = new Date()
  oneWeekAgo.setDate(oneWeekAgo.getDate() - 7)

  if (input.lastCarWashDate < oneWeekAgo) {
    throw new Error("We don't accept dirty cars")
  }
})

↑こうやって自分でバリデーションを定義することもできる(エラーをthrowすればいいだけ)

この場合詳細なメッセージをForm側にフィードバックするにはどうするんだろう

lemonadernlemonadern

form送信後にリダイレクトせず、内容をクリアしたい場合は、RHF の useForm を使う
onSubmitformMethos.reset() すると、クリアできる

lemonadernlemonadern
lemonadernlemonadern

基本的な認証機能は生成できる

yarn rw setup auth dbAuth

auth.ts はoverwrite, WebAuthn Support はNo

model User {
  id                  Int       @id @default(autoincrement())
  name                String?
  email               String    @unique
  hashedPassword      String
  salt                String
  resetToken          String?
  resetTokenExpiresAt DateTime?
}

Redwod にしたがってユーザモデルをつくる

      <Private unauthenticated="home">
        <Set wrap={ScaffoldLayout} title="Posts" titleTo="posts" buttonLabel="New Post" buttonTo="newPost">
          <Route path="/posts/new" page={PostNewPostPage} name="newPost" />
          <Route path="/posts/{id:Int}/edit" page={PostEditPostPage} name="editPost" />
          <Route path="/posts/{id:Int}" page={PostPostPage} name="post" />
          <Route path="/posts" page={PostPostsPage} name="posts" />
        </Set>
      </Private>

Route はプライベートで囲む、unauthenticated はリダイレクト先

lemonadernlemonadern

sdl のQuery, Mutation のフィールドで @SkipAuth, @requireAuth などをつけて、パーミッションを管理する

lemonadernlemonadern
  const { isAuthenticated, currentUser, logOut } = useAuth()

useAuth を使って認証関連の機能を取得できる

lemonadernlemonadern

.env にある SESSION_SECRET は、ブラウザに保存するCookieの暗号化キー

yarn rw g secret

で生成できる

lemonadernlemonadern

ポチポチやって、Prisma の接続先を変更して、マイグレーションをリセットしただけで動いた、すご

lemonadernlemonadern

Code

Netlify にデプロイするらしい
なんと、セットアップコマンドがある

yarn rw setup deploy netlify
lemonadernlemonadern

環境変数設定してボタンポチポチしたらデプロイできてしまいました、、、

lemonadernlemonadern

Chapter 5

Chapter 5 からが後半

Storybook, テストについて
https://redwoodjs.com/docs/tutorial/chapter5/storybook

ここからは https://github.com/redwoodjs/redwood-tutorial を使って作業する

lemonadernlemonadern

Chapter 6

これまでの総集編
ブログへのコメント機能を実装する

Chapter 7

Redwood で Role-Based Access Control をどうやるかのセクション

このスクラップは2022/10/21にクローズされました