Gatsby.js+TypeScriptでナビゲーションガードの超基礎的なやつ

要するにほとんどこれを日本語にして圧縮したようなものです。

が、今はTypeScriptの時代なので、ついでにこれをTypeScriptで書き換えます。あとついでにnpmではなくyarnを使っています。

あとこれを書いているのはReact自体超初心者なので、斧を投げられたら普通に頭蓋骨が割れて死にます。

何はともあれプロジェクト作成

Gatsbyのプロジェクトを作成してください。名前は公式チュートリアルに合わせてgatsby-authとします。

gatsby new gatsby-auth gatsbyjs/gatsby-starter-hello-world

最低限しか入ってないテンプレートなので、結構終わるのも早いはずです。

TypeScript対応

作成が終了したら、プロジェクトのディレクトリに移動して、早速JSXの作成……ではなく、TypeScript対応します。TypeScript対応も下と同じことをやっているだけです。

yarn add gatsby-plugin-typescript typescript
yarn add --dev @types/react @types/react-dom @types/node

上記が完了したら、gatsby-config.jsを探し出して、以下のようにpluginsに追加します。

// gatsby-config.js
module.exports = {
  // ...,
  plugins: [`gatsby-plugin-typescript`],
}

ここまで終わったら、TypeScript対応は完了です。他にもあるかもしれません。私を信じないでください。

一応gatsby developで起動するか確認しておいてください。画面上に「Hello World!」とだけ表示されたらOKです。

認証サービス作成

ページとかを作る前に、ちゃっちゃと認証サービスを作ってしまいます。ユーザー名とパスワードはハードコードしちゃいます。面倒なので。あなたの名前はjohnで、パスワードはpassです。src/servicesディレクトリの下にauth.tsを作成してください。これは特にJSX要素を含んでいないので拡張子もtsで大丈夫です。

// auth.ts
export const isBrowser = () => typeof window !== "undefined"

export const getUser = () =>
 isBrowser() && window.localStorage.getItem("gatsbyUser")
   ? JSON.parse(window.localStorage.getItem("gatsbyUser"))
   : {}

const setUser = user =>
 window.localStorage.setItem("gatsbyUser", JSON.stringify(user))

export const handleLogin = ({ username, password }) => {
 if (username === `john` && password === `pass`) {
   return setUser({
     username: `john`,
     name: `Johnny`,
     email: `johnny@example.org`,
   })
 }

 return false
}

export const isLoggedIn = () => {
 const user = getUser()

 return !!user.username
}

export const logout = callback => {
 setUser({})
 callback()
}

ページを一通り作っていく

次に一通りのページを作っていきます。作るページは以下の3つです。

・インデックス
・ログイン画面
・プロフィール表示

また、この3ページには共通したナビゲーションバーが存在するので、そちらをまず作成します。

src/componentsディレクトリの下にnav-bar.tsxを作成し、以下のように記述してください。

// nav-bar.tsx
import React from "react"
import { Link } from "gatsby"

export default () => (
 <div
   style={{
     display: "flex",
     flex: "1",
     justifyContent: "space-between",
     borderBottom: "1px solid #d1c1e0",
   }}
 >
   <span>You are not logged in</span>

   <nav>
     <Link to="/">Home</Link>
     {` `}
     <Link to="/">Profile</Link>
     {` `}
     <Link to="/">Logout</Link>
   </nav>
 </div>
)

Linkの行き先が/ですが、これはひとまずplaceholderです(日本語訳探したんですが、これっていうものが見つかりませんでした)。後でこの部分は書き換えます。

同じくsrc/componentsディレクトリの下にlayout.tsxを作成します。

// layout.tsx
import React from "react"

import NavBar from "./nav-bar"

const Layout = ({ children }) => (
 <>
   <NavBar />
   {children}
 </>
)

export default Layout

src/pagesディレクトリの下のindex.jsの名前をindex.tsxに変更し、layoutコンポーネントを使用するように書き換えます。

// index.tsx
import React from "react"

import Layout from "../components/layout"

export default () => (
 <Layout>
   <h1>Hello world!</h1>
 </Layout>
)

これでlocalhost:8000を開くと、先程作成したナビゲーションバーが表示されるようになりました。

さて、この調子でprofile.tsxlogin.tsxを作っていきたいところ……なのですが、これらはpagesディレクトリの下に作るのではありません。componentsディレクトリの下に作成します。なぜかというと、pagesの下に作ってしまうと、ナビゲーションガードができないからです。なので、Gatsby.jsの利点と言えば「pages下に置くと自動でルーティングしてくれる」ですが、自力でルーティングを書く必要が発生するんですね。
(本当はpages下に置いてもガードできるのかもしれませんが、チュートリアルだとこうやっているので……あとやり方を知らないので……)

チュートリアルだとloginの中身がクラスコンポーネントで書かれているのですが、せっかくナウヤングチョベリグなTypeScriptを使っているので、ここもHooksを使った関数型コンポーネントで書き換えます。

// profile.tsx
import React from "react"

const Profile = () => (
 <>
   <h1>Your profile</h1>
   <ul>
     <li>Name: Your name will appear here</li>
     <li>E-mail: And here goes the mail</li>
   </ul>
 </>
)

export default Profile
// login.tsx
import React, { useState, useEffect } from "react"
import { navigate } from "gatsby"
import { handleLogin, isLoggedIn } from "../services/auth"

export default () => {
 const [username, setUsername] = useState("")
 const [password, setPassword] = useState("")

 const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
   event.preventDefault()
   handleLogin({
     username,
     password,
   })
   navigate(`/app/profile`)
 }

 useEffect(() => {
   if (isLoggedIn()) {
     navigate(`/app/profile`)
   }
 })

 return (
   <>
     <h1>Log in</h1>
     <form
       method="post"
       onSubmit={event => {
         handleSubmit(event)
       }}
     >
       <label>
         Username
         <input
           type="text"
           name="username"
           value={username}
           onChange={event => setUsername(event.currentTarget.value)}
         />
       </label>
       <label>
         Password
         <input
           type="password"
           name="password"
           value={password}
           onChange={event => setPassword(event.currentTarget.value)}
         />
       </label>
       <button type="submit">Log In</button>
     </form>
   </>
 )
}

実際は上2つに書いた内容がページとして表示されます。ではどのようにして表示するのか?ルートディレクトリの直下にgatsby-node.jsを作成し、以下のように記述してください。このファイルの説明は初心者のボロが出るのであまりできませんが……まあコメントを読んでみてください😉

// gatsby-node.js

// Implement the Gatsby API “onCreatePage”. This is
// called after every page is created.
exports.onCreatePage = async ({ page, actions }) => {
 const { createPage } = actions

 // page.matchPath is a special key that's used for matching pages
 // only on the client.
 if (page.path.match(/^\/app/)) {
   page.matchPath = "/app/*"

   // Update the page.
   createPage(page)
 }
}

次に、src/pagesの下にapp.tsxを作成します。

// app.tsx
import React from "react"
import { Router } from "@reach/router"
import Layout from "../components/layout"
import Profile from "../components/profile"
import Login from "../components/login"

const App = () => (
 <Layout>
   <Router>
     <Profile path="/app/profile" />
     <Login path="/app/login" />
   </Router>
 </Layout>
)

export default App

え、なんかpathに赤波線引かれてるんだけど!?と驚いた方もいらっしゃると思います。そうなんです、引かれるんです。チュートリアルではこのコードでも動くんです。なぜならJavaScriptなので。しかし今回はTypeScriptになったので、そのへん厳しくなっちゃったんですね。

ならばどうすればいいか。オリチャー(訳注:オリジナルチャート)を発動します。

まず、src/componentsディレクトリにroutes.tsxを作成し、以下のように記述します。

// routes.tsx
import React from "react"
import { navigate } from "gatsby"
import { isLoggedIn } from "../services/auth"

export const PrivateRoute = ({ component: Component, ...rest }) => {
 if (!isLoggedIn() && window.location.pathname !== `/app/login`) {
   navigate("/app/login")
   return null
 }

 return <Component {...rest} />
}

export const PublicRoute = ({ component: Component, ...rest }) => <Component {...rest} />

次に、app.tsxを修正します。

// app.tsx
import React from "react"
import { Router } from "@reach/router"
import Layout from "../components/layout"
import Profile from "../components/profile"
import Login from "../components/login"
import { PrivateRoute, PublicRoute } from "../components/routes"

const App = () => (
 <Layout>
   <Router>
     <PrivateRoute path="/app/profile" component={Profile} />
     <PublicRoute path="/app/login" component={Login} />
   </Router>
 </Layout>
)

export default App

これで赤波線も消えました!gatsby developでlocalhost:8000等にアクセスしてみましょう。最初は/app/profileにはアクセスできませんが、/app/loginからログインするとアクセスできるようになっていると思います。ナビゲーションガード自体はこれで完成です。喜べ。

リファクタリングとか最後の仕上げとか

いやナビゲーションバー動いてねえし!そこに気付いたあなたはIQが128くらいあります。なので直しましょう。nav-bar.tsxを以下のように修正します。

// nav-bar.tsx
import React from "react"
import { Link, navigate } from "gatsby"
import { getUser, isLoggedIn, logout } from "../services/auth"

export default () => {
 const content = { message: "", login: true }
 if (isLoggedIn()) {
   content.message = `Hello, ${getUser().name}`
 } else {
   content.message = "You are not logged in"
 }
 return (
   <div
     style={{
       display: "flex",
       flex: "1",
       justifyContent: "space-between",
       borderBottom: "1px solid #d1c1e0",
     }}
   >
     <span>{content.message}</span>
     <nav>
       <Link to="/">Home</Link>
       {` `}
       <Link to="/app/profile">Profile</Link>
       {` `}
       {isLoggedIn() ? (
         <a
           href="/"
           onClick={event => {
             event.preventDefault()
             logout(() => navigate(`/app/login`))
           }}
         >
           Logout
         </a>
       ) : null}
     </nav>
   </div>
 )
}

インデックスページもログイン状態によって表示を変えるようにします。

// index.tsx
import React from "react"
import { Link } from "gatsby"
import { getUser, isLoggedIn } from "../services/auth"

import Layout from "../components/layout"

export default () => (
 <Layout>
   <h1>Hello {isLoggedIn() ? getUser().name : "world"}!</h1>
   <p>
     {isLoggedIn() ? (
       <>
         You are logged in, so check your{" "}
         <Link to="/app/profile">profile</Link>
       </>
     ) : (
       <>
         You should <Link to="/app/login">log in</Link> to see restricted
         content
       </>
     )}
   </p>
 </Layout>
)

最後に、profile.tsxを書き換えます。

// profile.tsx
import React from "react"
import { getUser } from "../services/auth"

const Profile = () => (
 <>
   <h1>Your profile</h1>
   <ul>
     <li>Name: {getUser().name}</li>
     <li>E-mail: {getUser().email}</li>
   </ul>
 </>
)

export default Profile

完成です。喜べ。

終わりに

ハマりどころと言えば、app.tsxにおけるpathの赤波線くらいでしょうか。他の部分は基本的にチュートリアルの通りで大丈夫なので、ここさえ乗り越えれば我々もナビゲーションガード熟練者です。ハードルが低い。

コードを上げようかと思いましたが、GitLabにログインするのが面倒だったのでやめました。まあ大体チュートリアルの通りなので大丈夫だと思います。

この記事が気に入ったらサポートをしてみませんか?