Remixでアプリケーションを作成
Remixの環境構築
こちらを参考にしたら、すんなりできました。
Mantine UIの設定
基本的には、こちらのフレームワークガイドに従って設定していきます。
APIのIPアドレス
今回は、バックエンドにDjango Ninjaを使用しており、開発環境にはDockerを使用しています。
この場合のIPアドレスはDockerのIPアドレスになります。
root.tsxの編集
まずはroot.tsxを編集します。
今回はログイン処理をRemix上で行うようにしますので、そのために必要なコードにしていきます。
root.tsv
import '@mantine/core/styles.css';
import { Link, Links, Meta, Outlet, Scripts, ScrollRestoration, useLoaderData, useNavigate} from "@remix-run/react";
import { Box, Button, ColorSchemeScript, Container, Group, MantineProvider, PasswordInput, TextInput, Text } from '@mantine/core';
import { useEffect, useState } from 'react';
import classes from './HeaderMegaMenu.module.css'
import { useForm } from '@mantine/form';
import { LoaderFunctionArgs, json } from '@remix-run/node';
import { User } from './type';
interface FormValues {
email: string
password: string
}
export const loader = async ({ request }: LoaderFunctionArgs) => {
// loginしているかどうかを確認する
try {
const cookies = request.headers.get('Cookie'); // Cookieを使用する場合
const accessToken = cookies?.split(';').find(cookie => cookie.trim().startsWith("accessToken="))?.split('=')[1] // Cookieを使用する場合
if (!accessToken) {
return json({
setIsLoggedIn: false
})
}
// ログイン情報を取得(IPアドレスはDockerのIPアドレス)
const res = await fetch(`${process.env.API_URL}/api/author/auth/get_login_user`, {
method: 'GET',
headers: {
'accept': 'application/json',
'Authorization': `Bearer ${accessToken}`
}
})
if (res.ok) {
const data = await res.json()
return json({ userData: data })
} else {
return json({
setIsLoggedIn: false
})
}
} catch (error) {
return error
}
}
export default function App({ children }: { children: React.ReactNode }) {
const navigate = useNavigate()
const { userData: userData } = useLoaderData<{ userData: User }>()
const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false)
const [error, setError] = useState<string | null>(null)
const [user, setUser] = useState<User | null>(null)
useEffect(() => {
const cookies = document.cookie // Cookieを使用する場合
const accessToken = cookies?.split(';').find(cookie => cookie.trim().startsWith("accessToken="))?.split('=')[1] // Cookieを使用する場合
if (accessToken) {
setIsLoggedIn(true)
setUser(userData)
navigate("/contacts")
} else {
setIsLoggedIn(false)
navigate("/")
}
}, [navigate])
const form = useForm<FormValues>({
mode: 'uncontrolled',
initialValues: {
email: '',
password: '',
},
validate: {
email: (value) => (/^\S+@\S+$/.test(value) ? null : 'Invalid email'),
password: (value) => (value.length < 1 ? "パスワードを入力してください" : null)
}
})
const handleLogin = async (values: FormValues) => {
try {
const res = await fetch('http://localhost:8000/api/author/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(values)
})
if (!res.ok) {
throw new Error('Login failed.')
}
const data = await res.json()
document.cookie = `accessToken=${data.access_token}; path=/`
setIsLoggedIn(true)
navigate('/contacts') // ログイン後の画面遷移
} catch (error) {
setError('Login failed')
}
}
// logout
const handleLogout = async () => {
// cookieを使用する場合
try {
document.cookie = "accessToken=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"
setIsLoggedIn(false)
navigate("/")
} catch (error) {
setError('Logout failed.')
}
}
return (
<html lang="ja">
<head>
<meta charSet="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1" />
<Meta />
<Links />
<ColorSchemeScript />
</head>
<body>
<MantineProvider>
<Container miw={700} h={200} mt={20}>
<div className='item-center'>
<header className={classes.header}>
<div className='flex item-center'>
<Button component={Link} to="/contacts" mr={15} mb={15}>To contacts</Button>
<Button component={Link} to="/articles" mr={15} mb={15}>To articles</Button>
{isLoggedIn ?
<div className='inline-block mx-auto'>
<Text className='inline-block mx-auto'>{user?.username}さん</Text>
<Button component={Link} onClick={handleLogout} to="/auth" mr={15} mb={15}>LogOut</Button> {/* true(login中)のとき */}
</div>
:
<Button component={Link} to="/auth" mr={15} mb={20}>LogIn</Button> // false(logout中)のとき
}
</div>
</header>
</div>
{children}
{isLoggedIn ?
<></>
:
<Box maw={340} mx="auto">
<form onSubmit={form.onSubmit(handleLogin)}>
<TextInput
withAsterisk
label="メールアドレス"
placeholder="your@email.com"
mb={20}
{...form.getInputProps('email')}
/>
<PasswordInput
placeholder="パスワード"
mb={20}
{...form.getInputProps('password')}
/>
{error && <div style={{ color: 'red' }}>{error}</div>}
<Group justify="center" mt="md">
<Button type="submit">Login</Button>
</Group>
</form>
</Box>
}
<Outlet context={userData} /> {/* ここに作ったルーティングが配置される */}
</Container>
</MantineProvider>
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
loader
ここでは、loader関数を使用して、ページ読み込み時にデータを取得するようにしています。
これができるのがloader関数です。
loader
export const loader = async ({ request }: LoaderFunctionArgs) => {
// loginしているかどうかを確認する
try {
const cookies = request.headers.get('Cookie'); // Cookieを使用する場合
const accessToken = cookies?.split(';').find(cookie => cookie.trim().startsWith("accessToken="))?.split('=')[1] // Cookieを使用する場合
if (!accessToken) {
return json({
setIsLoggedIn: false
})
}
// ログイン情報を取得(IPアドレスはDockerのIPアドレス)
const res = await fetch(`${process.env.API_URL}/api/author/auth/get_login_user`, {
method: 'GET',
headers: {
'accept': 'application/json',
'Authorization': `Bearer ${accessToken}`
}
})
if (res.ok) {
const data = await res.json()
return json({ userData: data })
} else {
return json({
setIsLoggedIn: false
})
}
} catch (error) {
return error
}
}
今回は、ページ読み込み時にログインユーザーの情報を取得するものですが、Remixでは、このようにloader関数を追加することで、ページの読み込み時にデータの取得や準備を行うことができます。
loader関数はサーバーサイドで実行され、クライアントに必要なデータを提供します。
loader関数は、loader.tsファイル内に記述され、LoaderFunctionという特別な型を持ちます。
詳細はこちらを確認してください。
useForm(Mantine UI)
レイアウトなどにはMantine UIを使用していますが、その中からuseFormを使用しています。
const form = useForm<FormValues>({
mode: 'uncontrolled',
initialValues: {
email: '',
password: '',
},
validate: {
email: (value) => (/^\S+@\S+$/.test(value) ? null : 'Invalid email'),
password: (value) => (value.length < 1 ? "パスワードを入力してください" : null)
}
})
使い方などは、こちらのドキュメントを参照してください。
Outlet
Remixでは、Outletを使用することで、Outletを配置した部分に子ルートを配置することができます。
また、contextを設定することで、その配置された子ルートに値を渡すことができます。
<Outlet context={userData} />
今回は、contextとしてloaderから取得したログイン中のユーザーデータを格納したuserDataをcontextとして渡しています。
詳細はこちら。
article.tsx
atricles.tsx
import { LoaderFunctionArgs, json } from "@remix-run/node";
import { useLoaderData, useOutletContext } from "@remix-run/react";
import { Article } from "~/type";
import { Text } from '@mantine/core'
export const loader = async ({ request }: LoaderFunctionArgs) => {
try {
const res = await fetch(`${process.env.API_URL}/api/press/articles`, {
method: 'GET',
headers: {
'accept': 'application/json',
}
})
const articles = await res.json()
return json({ articles })
} catch (error) {
return error
}
}
export default function Articles() {
const { articles } = useLoaderData<{ articles: Article }>()
const userContext: User = useOutletContext()
return (
<>
<h1>Article</h1>
{context.articles}
<div className="flex flex-col gap-4">
<Text>{userContext?.username}さんの記事</Text>
{articles ? (
<ul>
{articles.map((article: Article) => (
<>
<div className="flex flex-col gap-2">
<h2 key={article.id}>title: {article.title}</h2>
<p>No.{ article.id}</p>
</div>
<p key={article.id}>author: {article.author.username}</p>
</>
))}
</ul>
) : (
<i>No article</i>
)}
</div>
</>
)
}
routesディレクトリの直下にファイルを作成することで、このarticles.tsはhttp://localhost:3000/articlesを生成します。
また、root.tsxで取得したログイン中のユーザー情報をcontextで渡しています。
これを、useOutletContextで取得し、htmlに表示しています。
詳しくはこちらを参照してください。
所感
これまでNext.jsを使ったことはありましたが、web標準に準拠しているフレームワークなだけあって、フレームワーク独特の構文等が少なく、すんなり使えるイメージです。
最初は仕組みなどを理解する必要はありますが、将来的には他のフレームワークなどでも生かせるものが得られるフレームワークだと感じました。
今後は何かサイトやwebアプリなどにもトライしてみたいと思います。
Discussion