logo

Remixで簡単なアプリを作成しました

2024/1/25

概要

Remixを使ってみたかったので簡単なアプリを作ってみました。 趣味でドラムをやっていますが、どれくらい時間を使って頑張っているかを記録しておきたかったので、 google calenderに登録したドラム関連予定を取得し何時間頑張ったかを表示してくれる簡単なアプリです。

drumApp

78時間も!

アプリの処理の流れ

Google Sheetsにも記録しておきたかったので、二段構えになりました。

  1. [Google Apps Script] Google calendar APIを使ってカレンダーからドラム関連の予定を取得
  2. [Google Apps Script] 取得したデータをGoogle sheets APIを使ってGoogle Sheetsに書き込み
  3. [Remix] Google sheets APIを使ってGoogle Sheetsからデータを取得
  4. [Remix] 時間を計算して画面に表示

google sheetsに書き込まれるデータの例

categorystartDateTimeendDateTimehoursummary
drumFri May 1 18:30:00 GMT+09:00 2023Fri May 12 19:30:00 GMT+09:00 20231ドラム
drumFri May 2 17:30:00 GMT+09:00 2023Fri May 12 19:30:00 GMT+09:00 20232ドラム

アプリの詳細

Google Apps Scriptの動作は詳しく触れずにRemixのコードだけ触れることにします。 ちなみにsheetsに書き込む処理はAPI keyなどではできず、OAuth認証が必要になっため、そこの処理を書くのがやや手間でした。

Remix

Remixについて詳細は別の記事にまとめようと思っています。Next.jsとも比べて書いてみたいですね。 ここではRemixデータのフローの考えについてまとめました。

Remixの思想?としては

  • Web標準のものを積極的に使う
  • state管理はサーバーで

Remixはクライアント側でstateを持って管理するより、サーバで管理することを基本の考えてとして持っています。 その考えはこちらのドキュメントhow remix simplifies stateにも書いてあります。

サーバの状態をクライアント側にロードし、状態を変更したい場合は、サーバの状態を変更してその変更を再度fetchしてレンダリングさせるという流れになります。 ソースコードをみながらdata flowを確認してみます。

Remixのdata flow (loader component actionの動作の流れ)

Remix fullstack data flow からソースコードを持ってきました。

import type { ActionFunctionArgs, LoaderFunctionArgs, } from "@remix-run/node"; // or cloudflare/deno import { json } from "@remix-run/node"; // or cloudflare/deno import { useLoaderData } from "@remix-run/react"; export async function loader({ request, }: LoaderFunctionArgs) { const user = await getUser(request); return json({ displayName: user.displayName, email: user.email, }); } export default function Component() { const user = useLoaderData<typeof loader>(); return ( <Form action="/account"> <h1>Settings for {user.displayName}</h1> <input name="displayName" defaultValue={user.displayName} /> <input name="email" defaultValue={user.email} /> <button type="submit">Save</button> </Form> ); } export async function action({ request, }: ActionFunctionArgs) { const formData = await request.formData(); const user = await getUser(request); await updateUser(user.id, { email: formData.get("email"), displayName: formData.get("displayName"), }); return json({ ok: true }); }

大きく三つ loader component actionのパートに分けられます。

loader

  • いわゆるget
  • loaderを使ってデータを取得します。
  • web標準のRequest Responseを使用します。
  • server側で一回だけ動作します。

Component

  • いわゆるview
  • <form /> <input />などのフォームとsubmit buttonでクライアントのデータをサーバーに送れます。

action

  • いわゆるpost
  • submitされたら標準APIのrequest.formData()で入力した値が取れます。

絵で表すとこうなります。 一つのファイルで簡単に初回のデータを取得し、入力されたデータを標準APIを使って処理することができます。ライブラリを使ってstateを管理する必要はないです。

dataflow dataflow2

作ったアプリでのdata folw

自分のアプリではactionは不要だったのでこんな感じでした。内容はやや省略しています。

export const loader = async () => { // google sheets apiを使ってデータを取得 const response = await fetch( `https://sheets.googleapis.com/v4/spreadsheets/${sheetId}/values/${sheetName}?key=${apiKey}`, ); if (response.ok) { const result = await response.json(); // sheetsデータから必要なデータだけ整形 const [headers, ...values] = result.values; const objectResult: SheetData = values.map((row) => { return headers.reduce((obj, header, index) => { obj[header] = row[index]; return obj; }, {}); }); return calculateHour(objectResult, "drum"); } else { console.error("Failed to fetch data from Google Sheets"); return null; } }; const Growingup = () => { // データをロードして画面に表示 const { hour, categoryName } = useLoaderData<typeof loader>(); return ( <> <div className={styles.wrapper}> <img className={styles.image} src={drumImage} alt="drum" /> <div className={styles.hour}> <p> <span className={styles.hourNumber}>{hour}</span> <span className={styles.hourText}>h</span> </p> </div> </div> </> ); };

vanilla-extract

vanilla-extractも噂だけ聞いていたので試してみました。 vanilla-extractを以下のように紹介しています。

"Zero-runtime Stylesheets in TypeScript."

堅安全性を持つcssがかけます。 書き方もcss moduleと似ているのでcss moduleを書いた人であればすぐになれると思います。

たくさん書いてはないですが結構いい感じでした。

styles.css.ts
export const hour = style({ display: "flex", alignItems: "center", justifyContent: "center", position: "absolute", top: "50%", left: "50%", transform: "translate(-50%, -50%)", textWrap: "nowrap", });

基本の書き方はこのようになっています。

styles.css.ts
const textStyle = (color, fontSize) => style({ color, fontSize, fontWeight: "bold", display: "inline-block", transform: "skewX(-8deg)", }); export const hourNumber = textStyle("#E9E34A", "5.5rem"); export const hourText = textStyle("white", "3rem");

このように変数としてを渡してstyleを作ることもできます。便利でした。

import * as styles from "../style.css"; return ( <p> <span className={styles.hourNumber}>{hour}</span> <span className={styles.hourText}>h</span> </p> )

css moduleみたいにimportしてclassにstyleを付与します。

最後

今回はシンプルなアプリですが、よりいろんなデータを処理する大きなアプリでRemixを試したいと思いました。

Ref