RedwoodJSに入門してみた(第3回: CRUD作成 WEB編)
この記事について
この記事は、全5回の第3回です。
RedwoodJSに入門してみた(第1回: アプリ作成〜モデル作成)
RedwoodJSに入門してみた(第2回: CRUD作成 API編)
RedwoodJSに入門してみた(第3回: CRUD作成 WEB編)
RedwoodJSに入門してみた(第4回: dbAuthによる認証)
RedwoodJSに入門してみた(第5回: 実際に触ってみて感じたこと)
前回に引き続きRedwoodJSのScaffoldで作成されたファイルを見ていく。
今回はweb
配下を中心に見ていく
Router
Redwoodには独自のRouterが内蔵されている。
ルーティングの仕組みはシンプルで、現在のURLが<Route>
コンポーネントのpath
に一致すれば、そのpage
をレンダリングし、一致するものがなければnotfound
が指定されている<Route>
のpage
をレンダリングする。
- import { Router, Route } from '@redwoodjs/router'
+ import { Set, Router, Route } from '@redwoodjs/router'
+ import ScaffoldLayout from 'src/layouts/ScaffoldLayout'
const Routes = () => {
return (
<Router>
+ <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>
<Route notfound page={NotFoundPage} />
</Router>
)
}
Scaffold実行後にはPost関連のルーティングが追加されている
Layout関連として、下記2つのファイルが作成されている
web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx
web/src/scaffold.css
さらにweb/src/App.tsx
にcssのインポート文が追加されているが、cssに関してはここでは割愛する
Route
まずは<Route>
コンポーネントについて。
各<Route
>コンポーネントにはpath
,page
,name
を渡す必要がある。ここで渡したname
から名前付きルート関数の名前がつけられる。
例えば、下記のようなRouteを追加した場合、routes
にhome()
という関数が追加され、routes.home()
は文字列として/
を返すようになる。
const Routes = () => {
return (
<Router>
<Route path="/" page={HomePage} name="home" />
</Router>
);
};
import { Link, routes } from "@redwoodjs/router";
// <a href="/">が返される
const SomePage = () => <Link to={routes.home()} />;
Layout
次に、生成された<Layout>
コンポーネントについて。
<ScaffoldLayout>
はweb/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx
に生成されているLayoutコンポーネントで、デフォルトではPost内で遷移するためのシンプルなヘッダーのようなものが作られている。
Layoutは下記コマンドで作成することもできる
yarn rw g layout foo
実行するとweb/src/layouts/FooLayout
配下に3つのファイルが作成される
- FooLayout.stories.tsx
- FooLayout.test.tsx
- FooLayout.tsx
Set
Layoutが渡されている<Set>
コンポーネントについて。
<Set>
コンポーネントにはLayoutを渡すことができる。例えば、Scaffoldで追加された下記の<Set>
コンポーネントを見てみよう。
const Routes = () => {
return (
<Router>
<Set
wrap={ScaffoldLayout}
title="Posts"
titleTo="posts"
buttonLabel="New Post"
buttonTo="newPost"
>
<Route path="/posts/new" page={PostNewPostPage} name="newPost" />
...
</Set>
</Router>
);
};
<Set>
コンポーネントのwrap
にScaffoldLayout
が渡されている
<Set>
のwrap
に渡したコンポーネントは下記のように展開され、wrap
以外のprops
はラッパーとして渡したコンポーネントに引数として渡される
const Routes = () => {
return (
<Router>
<ScaffoldLayout
title="Posts"
titleTo="posts"
buttonLabel="New Post"
buttonTo="newPost"
>
<Route path="/posts/new" page={PostNewPostPage} name="newPost" />
...
</ScaffoldLayout>
</Router>
);
};
だったら<Set>
を使わずに<ScaffoldLayout>
で囲めば良いのでは?という感じもするが、<Set>
を使うことで、同じ<Set>
内のページ間で再レンダリングが起こらなかったり、認証をチェックする機能があったりなど、いくつかのメリットがあるので基本的には<Set>
を使うのが望ましい。
Page
次は<Route>
コンポーネントのpage
で指定しているPageコンポーネントについて。
Scaffoldの実行で下記の4つが追加されている
web/src/pages/Post/EditPostPage/EditPostPage.tsx
web/src/pages/Post/NewPostPage/NewPostPage.tsx
web/src/pages/Post/PostPage/PostPage.tsx
web/src/pages/Post/PostsPage/PostsPage.tsx
ここで追加されたPage
はそれぞれ対応するコンポーネントを返している
例えばEditPostPage.tsx
は下記のようになっている
import EditPostCell from "src/components/Post/EditPostCell";
type PostPageProps = {
id: string;
};
const EditPostPage = ({ id }: PostPageProps) => {
return <EditPostCell id={id} />;
};
export default EditPostPage;
Postの編集に必要なid
を受け取り、それをEditPostCell
に渡している。
さて、改めてRoutes.tsx
に追加された内容を見てみよう。下記のようになっているはずだ。
import { Set, Router, Route } from "@redwoodjs/router";
import ScaffoldLayout from "src/layouts/ScaffoldLayout";
const Routes = () => {
return (
<Router>
<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>
<Route notfound page={NotFoundPage} />
</Router>
);
};
page
で指定されているPostNewPostPage
やPostEditPostPage
はどこからもインポートされていないように見えるが、実はRedwoodはRoutes.tsx内のPageコンポーネントについては自動的にインポートしてくれるので、インポート文を書かなくても呼び出せる。
余談だが、Redwoodがインポートするとはいえ、エディタで怒られないのが不思議だなと思っていたら、pages
配下のPageコンポーネントがグローバル変数化されていた。
ちなみにPageの命名は、ディレクトリ名とファイル名を結合したものをもとにつけられているようだ(単純な結合ではないので注意)
web/src/pages/HomePage/HomePage.tsx
だったらHomePage
、web/src/pages/Post/NewPostPage/NewPostPage.tsx
だったらPostNewPostPage
のような感じ。
コマンドで作成する場合
PageもLayoutと同様にgenerateコマンドで作成できる
yarn rw g page home /
のように、コマンドを実行するとweb/src/pages/HomePage
配下に3つのファイルが作成される
HomePage.stories.tsx
HomePage.test.tsx
HomePage.tsx
Page
コンポーネントが作られるだけでなくRoutes.tsx
にも下記のRouteが追加される
<Route path="/" page={HomePage} name="home" />
これで、rootのパスにアクセスしたときに<HomePage>
が表示されるようになる
yarn rw g page about
のようにpathを指定しなかった場合は、下記のようにpage名をもとにしたpathが自動で設定される
<Route path="/about" page={AboutPage} name="about" />
Cell
CellはRedwood独自の仕様で、公式ドキュメントには、「データフェッチに対する宣言的なアプローチ」と書かれていた。
各コンポーネントで呼び出していたデータフェッチライブラリによる状態管理をまとめて管理しちゃおう的なやつだと思われる。
従来だとそれぞれのコンポーネントでGraphQLの実行とライフサイクル管理をしていた。
const Posts() => {
const { loading, error, data } = useQuery(GET_POSTS)
if (loading) return 'Loading...'
if (error) return `Error! ${error.message}`
return (
<ul>
{data.posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
しかしRedwoodではCellがGraphQLの実行とライフサイクル管理を行うので、それぞれのコンポーネントはデータの状態を気にしなくてよい。
Scaffoldでは下記の3つのCellが追加されている。
web/src/components/Post/PostCell/PostCell.tsx
web/src/components/Post/PostsCell/PostsCell.tsx
web/src/components/Post/EditPostCell/EditPostCell.tsx
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: String!) {
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} />;
};
Cellは決められた名前で名前付きエクスポートする必要がある。
Cellが呼び出されるとRedwoodがQUERY
を実行して、レスポンスが来るまでLoading
を表示、その後レスポンスに応じて、Empty
, Failure
, Success
を返す。
Empty
コンポーネントをエクスポートしなかった場合はデフォルトで空のコンポーネントが定義される。
コンパイル時に何が行われているのやら、黒魔術感がすごいけど、Successコンポーネント内でデータを扱えば、loadingの状態を考慮しなくていいのはありがたい。
ちなみにRedwoodがCellを判断する基準は下記3点
- サフィックスにCellとつくファイル名であること
- QUERYが名前付きエクスポートされていること
- デフォルトエクスポートが定義されていないこと
基本的にコマンドで作成すれば、Cellの要件を満たさないということはなさそう。
超レアケースだが、Cell以外のファイルでサフィックスにCellをつけたくなったときに注意が必要。
コマンドで作成する場合
Cellもpageやlayoutと同様にgenerateコマンドで作成できる
yarn rw g cell user
とするとweb/src/components/UserCell
配下に4つのファイルが作成される
- UserCell.mock.ts
- UserCell.stories.tsx
- UserCell.test.tsx
- UserCell.tsx
今回はUser単体を取得するCellを作成したが、ユーザーの一覧などlistで取得したい場合は
yarn rw g cell users
のように複数形で指定するか、または複数形にできない名詞などはyarn rw g cell fish --list
とすることでlistで取得するCellを作成できる
Component
その他、表示に関わる下記ファイルが生成されている
web/src/components/Post/NewPost/NewPost.tsx
web/src/components/Post/Post/Post.tsx
web/src/components/Post/PostForm/PostForm.tsx
web/src/components/Post/Posts/Posts.tsx
ちなみに、ここでのFormはHTMLのFormで作成されているがRedwoodには組み込みのFormライブラリもある。
コマンドで作成する場合
コンポーネントの作成はyarn rw g component Article
といったコマンドで、page
やlayout
の作成と同様にweb/src/components/Article
配下に3つのファイルが作成される
- Article.stories.tsx
- Article.test.tsx
- Article.tsx
Package
Scaffoldでpackageも追加されている。
web/package.json
+ "humanize-string": "2.1.0",
fooBar
とかfoo_bar
のような文字列をFoo bar
みたいに変換してくれるやつらしい。
こういうのもScaffoldで入れられるのは個人的にはあんまり嬉しくない。
web/src/lib/formatters.tsx
内で定義されている関数で使用されていて、enumの値などをフォーマットして表示するのに使われる想定のようだが、日本語表示したい場合は必要なさそう。
補足
エディタ上のエラーについて
types/graphql
から型をインポートしているいくつかの箇所で、モジュール'types/graphql'またはそれに対応する型宣言が見つかりません。ts(2307)
というエラーがエディタ上で出ていた。環境要因かもしれないが、対象のインポート文を一時的にコメントにして保存、その後コメントアウトして保存という動作をすると直った…。この場合のtypes
もsrc
ディレクトリみたいに現在のworkspace配下のtypes
ディレクトリを見ているっぽいが、初回の読み込み時には対象のディレクトリを見つけられないのかもしれない。
自動生成されるファイル名について
Redwoodで作成されるファイル名は少し冗長な命名になっている
例えばScaffoldLayout配下にあるのに、ファイル名もScaffoldLayout.tsxになっているけど、これはlayouts/ScaffoldLayout/index.tsx
でもいいのではないかと思った人もいるかも知れない。しかし、エディタで複数ファイルを開いたときにどのファイルを開いているか分かりやすいように、あえてファイル名の重複をなくしていたり、React Developer Tools使用時の視認性を考慮していたりで、そういうファイル名になっているらしい。
意外とディレクトリ名とファイル名に重複があっても気にならないし、逆に、index.tsが複数あってエディタの上部にはファイル名しか載ってないせいでindex.tsが複数あって、よく見たら違うファイルだったという経験もあるので、これは良いかもしれない。
次回
次回は認証を実装していく。
Discussion