RedwoodJS Tutorial をなぞる
これをやる
2022-10-28 追記
Chapter 1
JS と TS のコード片が両方用意されていて親切な感じ
生成
yarn create redwood-app --ts ./my--redwoodblog
cd ./my--redwoodblog
yarn rw dev
でデフォルトページが見れる。こんなの書くか迷ったけどいちおうメモしておく
デフォルトポートは 8, 9, 10
!!! で覚えてね、とのこと
特に意味はないっぽい
.nvmrc
とか.editorconfig
、.vscode/
とかも生成されていてびっくりした、そんなのまで生成するんか
- メインになるのは
api/
,scripts/
,web/
のディレクトリで、このうちapi/
(バックエンド) とweb/
(フロントエンド) は yarn workspace で分けられている - RedwoodJSでは、こいつらを sides と呼ぶらしい
workspace で分けられているので、もちろんパッケージ追加時はワークスペース指定が必要
yarn workspace web add marked
-
scripts/
はNodeランタイムで動かすスクリプトを置く場所
デフォルトでは、DBのシードをおこなうseed.ts
が置いてある
api/
-
db/
は Prisma のスキーマ・マイグレーションファイルの場所 -
src/
-
directives/
: GraphQL の Schema Directive -
functions/
: 自動生成されるもの以外だと、lambda functions を置く -
graphql/
: GQL のスキーマ定義ファイルを置く -
lib/
: 名前の通り -
services/
: ビジネスロジックを書くファイルを置く
-
web/
こちらはだいたい名前そのまま
web/src/
├── App.tsx
├── components/
├── index.css
├── index.html
├── layouts/
├── pages/
└── Routes.tsx
ページをつくる
Scaffolding
yarn redwood generate page home /
すると👇
web/src/pages/HomePage/
├── HomePage.stories.tsx
├── HomePage.test.tsx
└── HomePage.tsx
が作られ、 Routes.tsx
も更新される
[解決済]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
とかは当たり前に動くので、エディタ上のチェックだけに引っかかっている感じ。これを回避する設定があるだろうと思ったが、見つからなかった...
調べる
Auto Imports on Routes.js
のところにごちゃごちゃ書いているけど、画一的な解決策を提示してくれているわけではない
似たようなIssueもなさそうな感じだった
あと、コンポーネントをimportしないせいで出てくるTSコンパイラのエラーを消す方法がわからなくて困っています。。
https://zenn.dev/link/comments/261aa5fdc9dc57
おなじとこで困ってる人いてうける
どうしよう
明示的にimportしても動くのだけど、
// 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage
みたいな挙動があるので、こちら側で上書きするのはちょっとやめておきたい
Route
はJSファイルにする」を一旦は採用しようかな
ちゃんとして解決策はチュートリアルやってから考えます
解決してしまった
違うことやっててVSCode再起動したらエラー消えちゃった
生成されたコードの情報をLSP(?)が読めてなかったのが原因ぽい
他にページ作ったときもすぐエラーが消えたので、読み込みさせれば解決
yarn redwood generate page about
これは、
yarn rw g page about
と置き換えられる
パスを指定しない場合は、ページの名前がパスに利用される
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 が適用される。楽すぎ
途中で気づいたけど、何も設定してないのに format on save も効いている
Chapter 2
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/ で見られる
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ができてる、、、俺の書いたコードはスキーマ定義だけなのに
Cell
のクエリに型エラーが表示される場合があるときは
- VSCodeのリロード
-
yarn rw g types
で型定義生成
のどちらかをすれば治るよ、とのこと
Cell
RedwoodJS では、非同期処理が必要なページの構成に Cell
と呼ばれる仕組みを使う
以下はScaffoldされたPostCellの例
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で呼ばれているだけ
import PostCell from 'src/components/Post/PostCell'
type PostPageProps = {
id: number
}
const PostPage = ({ id }: PostPageProps) => {
return <PostCell id={id} />
}
export default PostPage
これだけで表示される
Cell とは
Cell は、事前に定められた名前を持つ定数の名前付きエクスポートをすると、いい感じに Redwood 側でそれを利用してくれる機能
QUERY
, Loading
, Empty
, Failure
, Success
を書くと、勝手にそれが使われる
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 + 配置すれば表示される。簡単すぎ
query ArticlesQuery {
articles: posts {
id
title
body
createdAt
}
}
クエリの値に別名を付けると、それをSccessのところで利用可能
export const Success = ({ articles }: CellSuccessProps<ArticlesQuery>) => { /* ... */ }
Service
services/**
ディレクトリに リゾルバを書くと、それがSDLにマップされて利用可能になる。
ビジネスロジックはここに書く
リゾルバを書かなければただの関数として扱えるので、柔軟性はありそう
記事ページをつくる
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}
は 整数しか受け取らない
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`
lint, format on Save してくれるのは嬉しいけど、 unused import を自動で消すやつも入れてほしい
Chapter 3
From をつくる
- RedwoodJS 自身が Form 関連の機能も提供している
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の機能を備えたコンポーネントを標準で利用できる
そういえば、このチュートリアルだと型定義に interface を使うほうに寄せてるみたい
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.
Formから送られてきたデータを保存する
- スキーマ定義
- migrate
-
yarn rw g sdl Contact
で、スキーマからSDL(Schema Definition Language)生成
⇨ Redwoodが勝手にResolverにmappingする
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.
id
や createdAt
はユーザの入力で設定されないことを想定しているため、自動生成のDSLにはこれらが含まれていない
scaffold した後に生成されたファイルを開くとしばしば赤線エラーが出るけど、例によってエディタをリロードすれば正しく読み込まれて解決する
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 ディレクトリの同名の関数にマッピングされる
読み取りのみのSDLを生成する場合(CRUDが必要ない場合)
yarn rw g sdl Contact --no-crud
- Query および Mutation の操作に認証が必要ない場合、
@requireAuth
を@SkipAuth
にすればいい - 生成されたSDLに必要ないものがあれば、消去する
なんと、開発サーバと一緒に GraphQL Playground が立ち上がっているので GraphQL レベルで動作確認できる(http://localhost:8911/graphql )
すご
コンポーネントから呼ぶ方法
- Mutation を 定数で定義
-
useMutation
hook の引数に、1 で定義したものを渡す - hook の返り値のfunctionを onSubmit で呼び出して使う
Formの改善
- ダブルクリックで複数送信になるのを防ぐ
-
useMutation
の返り値からloading
を得て、Submit ボタンに使う
-
- 成功時に通知する
-
useMutation
のonCompleted
と redwood のtoast
を使う
-
サーバーサイドバリデーション
// ...
validate(input.email, 'email', { email: true })
// ...
validate(チェック対象, フォームのname属性, バリデーションディレクティブを含むオブジェクト)
当たり前だが、GraphQLを挟んでいる時点でプロパティの存在は保証できている(DSLでそのように定義しているなら)
サーバーエラーをフォームにフィードバックする
/* ... */
<Form onSubmit={onSubmit} config={{ mode: 'onBlur' }} error={error}>
<FormError error={error} wrapperClassName="form-error" />
{/* ... */}
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側にフィードバックするにはどうするんだろう
form送信後にリダイレクトせず、内容をクリアしたい場合は、RHF の useForm
を使う
onSubmit
で formMethos.reset()
すると、クリアできる
Chapter 4
基本的な認証機能は生成できる
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
はリダイレクト先
sdl のQuery, Mutation のフィールドで @SkipAuth
, @requireAuth
などをつけて、パーミッションを管理する
const { isAuthenticated, currentUser, logOut } = useAuth()
useAuth
を使って認証関連の機能を取得できる
.env
にある SESSION_SECRET
は、ブラウザに保存するCookieの暗号化キー
yarn rw g secret
で生成できる
Deployment
Database
Railway のDBに接続する
ログインしなくても使えるらしい(ログインしない場合は24時間で消える)
Digital Ocean 知らなかった https://www.digitalocean.com/products/managed-databases
ポチポチやって、Prisma の接続先を変更して、マイグレーションをリセットしただけで動いた、すご
Code
Netlify にデプロイするらしい
なんと、セットアップコマンドがある
yarn rw setup deploy netlify
PrismaはOSに合わせたバイナリを生成するので、ローカルでビルドしてpushすると Netlify の環境との互換が無くて壊れることがある
互換があるのは debian-openssl-1.1.xか rhel-openssl-1.1.x
https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#binarytargets-options
ということで、おとなしくGitHub連携してNetlifyでビルドしろと書いてある
環境変数設定してボタンポチポチしたらデプロイできてしまいました、、、
Chapter 5
Chapter 5 からが後半
Storybook, テストについて
ここからは https://github.com/redwoodjs/redwood-tutorial を使って作業する
Chapter 6
これまでの総集編
ブログへのコメント機能を実装する
Chapter 7
Redwood で Role-Based Access Control をどうやるかのセクション