💿

Remix(Reactフレームワーク)でアプリを作った際に躓いたポイント6つ

2022/01/04に公開
2

話題のRemixを使って英単語アプリ「Repitan」をリリースしたのでRemixを使って躓いたところを6つ紹介しようと思います。
普段はNuxt(Vue)を使用することが多いのでこんなところで躓かないだろってところで躓いてると思います🥲もし間違っているところがあれば優しくご指摘いただけると嬉しいです🥺

Remixとは?


https://remix.run/

公式の最初の文では以下のことを述べています。

Focused on web fundamentals and modern UX, you’re simply going to build better websites
Remix is a full stack web framework that lets you focus on the user interface and work back through web fundamentals to deliver a fast, slick, and resilient user experience. People are gonna love using your stuff.
訳:ウェブの基礎とモダンなUXに焦点を当て、より良いウェブサイトをシンプルに構築することができます
Remixはフルスタックのウェブフレームワークで、ユーザーインターフェースに集中し、ウェブの基本に立ち返って、高速でスムーズ、かつ弾力性のあるユーザーエクスペリエンスを提供します。人々は、あなたの作品を喜んで使うことでしょう。

同じReactの人気フレームワークであるNextやGatsubyにある、SSGなどの静的ビルドを使わず、分散システムとネイティブブラウザの機能を活用することで、迅速なページロードと瞬時のトランジションを実現しています。
詳細は公式サイトを御覧ください💿💿💿

作ったアプリ


https://www.repitan.app/
流して覚える英単語アプリ「Repitan」を作成しました。
英語アプリってどれも自分で操作しないと次へ進めなかったりして私はどのアプリも長続きしませんでした。
そんな私のような英語勉強したいけどしたくない(?)人向けへ流すだけでなんとなく学習できるアプリを作成しました。つまり自分が欲しかっただけです。
自分もumiremixという名を名乗っているので、remix仲間としてRemixを使ってみたくて作成したアプリなんですが、正直Remixの良さはあまり出せていません…😇
使用した技術をざっくり書くと下記のような感じです。

Remixで躓いたところ

その1:コンポーネントを作る

「え、うそ…?公式ドキュメントでコンポーネントについて書いてないことってある…?」とびっくりしたのですが、まあ普通に作れば大丈夫でした。Remix公式だと当たり前すぎることは省略して書かれてる気がしています。なのでRemixの公式で書いてないことはReactでググれば大体なんとかなりました。

  1. コンポーネント用のディレクトリを作成
    mkdir app/components

  2. その配下に以下のようにファイルを作成。

app/components/button.tsx
import { Link } from "remix"

export function Button(props) {
  return (
    <Link className="button" to={props.to} data-button>
      {props.text}
    </Link>
  )
}
  1. コンポーネントを読み込みたいページに以下のように記述して完了
app/routes/index.tsx
import { Button } from "~/components/button"

export default function Index() {
  return (
    <Button to={`/`} text={`トップへ戻る`} />
  )
}

上のコンポーネントではpropsとCSSのscopedも使用しています。CSSをscopedしたい場合には、
<Link className="button" to={props.to} data-button>のようにdata-XXXXを追加、CSSファイルで[data-button].button {...}といった書き方でscopedさせます。

その2:動的に画像を読み込む

コンポーネントと同じく公式ドキュメントに書いて無くてびっくりしたのですが、こちらも普通に書けば大丈夫です。

  1. 画像用のディレクトリを作成
    mkdir app/images

  2. 画像を作成したディレクトリに入れます

  3. 画像を以下のように読み込んで完了

app/routes/index.tsx
import LogoImage from '~/images/logo.png'

export default function Index() {
  return (
      <h1 className='logo'>
        <img src={LogoImage} alt="Repitan | 流して覚える英単語" />
      </h1>
}

OGP画像などの静的で良い画像は/publicに置けばいいのですが、キャッシュが残らないようにするためには動的に画像を読み込む必要があるので上記のようにしてます。

その3:アニメーションをつける

最初CSSアニメーションで実装したところ、safariでのカクつき・遅延が発生してしまったので、react-springというモダンなReact用アニメーションのライブラリを見つけたので入れました。Remixとの相性が気になったのですが、問題なく使えました。

  1. react-springをインストール
    yarn add react-spring

  2. アニメーションを使用したいページやコンポーネントで以下のように記述して完了

app/routes/index.tsx
import { Menu } from "~/components/menu"
import { useSpring, animated } from 'react-spring'

export default function Index() {
  const [ style, api ] = useSpring(() => ({
    from: { width: '100%' },
    to: { width: '0%' },
    config: { duration: 8000 }
  }))
  const startAnimation = () => {
    api.stop()
    api.set({ width: '100%' })
    api.start({
      from: { width: '100%' },
      to: { width: '0%' },
      config: { duration: 8000 }
    })
  }
  const pause = () => {
    api.pause()
  }
  const play = () => {
    startAnimation()
  }
  const restart = () => {
    api.resume()
    startAnimation()
  }
  const finish = () => {
    api.stop()
  }
  return (
    <div>
      <button className="pause" onClick={pause}>一時停止</button>
      <animated.div className="time" style={style} />
      <Menu play={play} restart={restart} finish={finish} />
    </div>
  )
}

その4:スライドショーをつける

こちらもRemixとの相性が気になっていたのと、よくNuxtではお世話になっているswiperと迷ったのですが、swiperだとやりたいことに対して多機能すぎるかなと思いslickにしました。

  1. react-slickをインストール
    yarn add react-slick

  2. スライドショーを使用したいページやコンポーネントで以下のように記述して完了

app/routes/index.tsx
import Slider from "react-slick"
import { Courses } from "~/components/courses"

export const loader = async ({ params }) => {
  const course = await Courses.find((v) => v.slug === params.slug)
  if (!course) {
    throw new Response("Not Found", {
      status: 404
    })
  }
  const json = course.data.json
  return json
}

export default function Index() {
  const json = useLoaderData()
  const [currentCount, setCurrentCount] = useState(1)
  const [count, setCount] = useState(0)
  const settings = {
    dots: false,
    infinite: true,
    slidesToShow: 1,
    slidesToScroll: 1,
    arrows: false,
    autoplay: true,
    autoplaySpeed: 8000,
    pauseOnFocus: false,
    pauseOnHover: false,
    afterChange: () => {
      setCount(count + 1)
    },
    beforeChange: (current: number, next: number) => {
      let currentCount
      currentCount = next + 1
      setCurrentCount(currentCount)
    },
    onInit: async() => {
      setCount(count + 1)
    }
  }
  const customeSlider = useRef()
  const prev = () => {
    customeSlider.current.slickPrev()
  }
  const next = () => {
    customeSlider.current.slickNext()
  }
  const pause = () => {
    customeSlider.current.slickPause()
  }
  const play = () => {
    customeSlider.current.slickPlay()
  }
  const restart = () => {
    customeSlider.current.slickPlay()
    customeSlider.current.slickGoTo(0)
  }
  return (
    <div>
      <Slider ref={customeSlider} {...settings}>
        {json.map((data, item) => (
          <div>
             <p className="question">{data.en}</p>
             <p className="answer">{data.ja}</p>
          </div>
        ))}
      </Slider>
    </div>
  )
}

その5:404ページを作る

こちらは公式ドキュメントの説明では少しわかりにくかったのですが、以下のような手順で良さそうです。正確には404ページを作るというより404用のレイアウトを作るの方が合ってそうな気もします。

app/root.tsx
import { useCatch } from "remix";
import { Error } from "~/components/error"

export function CatchBoundary() {
  let caught = useCatch();
  let message;
  switch (caught.status) {
    case 404:
      message = (
        <span>お探しのページが<br />見つかりませんでした</span>
      );
      break;
    default:
      throw new Error(caught.data || caught.statusText);
  }
  return (
    <html lang="ja">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width,initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        <Error title={caught.status} subtitle={caught.statusText} message={message} />
      </body>
    </html>
  );
}

その6:クリップボードにコピーする

※公開してから気がついたのですが、もう一つ躓いたところがあったので追加しました。
何故かクリップボードAPIが使えなかったので、react-copy-to-clipboardを使用しました。こちらに関しては私の記述の問題だと思うのでクリップボードAPI使えたよ!って方はコメントいただけると嬉しいです😊

app/components/share.tsx
import { CopyToClipboard } from 'react-copy-to-clipboard'
import { useState } from 'react'

export function Share(props) {
  const url = `https://repitan.app`
  const hashtags = `repitan`
  const text = `流して覚える英単語アプリRepitanで${props.count}回英単語を勉強しました!💫`
  const [value, setValue] = useState(`${text} ${url} #${hashtags}`)
  return (
    <div data-share>
      <textarea 
        name="textarea" 
        id="textarea"
        className="textarea"
        value={value}
        onChange={e => setValue(e.target.value)}
      />
      <CopyToClipboard text={value} onCopy={props.onCopy}>
        <button className="copy">テキストをコピー</button>
      </CopyToClipboard>
    </div>
  )
}

Remixを使ってみた感想

現時点では公式のドキュメントが簡素なので、Reactに慣れている人向けだなと感じました。ググってもあまり出てこない上に、remixという単語だとmetamaskの方がひっかかったりしてややこしかったです。Remixの考え方は好きなのでドキュメントの充実など、今後の発展に期待しています。

Discussion

aiji42aiji42

Remixだと何故かクリップボードAPIが使えなかったので

Remixだからnavigator.clipboardが動かないということはないと思います。
単純にサーバサイドレンダリングし、クライアント側でハイドレートするというコンセプトは、Next.jsを始めとした、他のサーバサイド対応のReact系のフレームワークと変わりませんので、クライアントネイティブな関数が動かないということは無いはずです。
実際の動かなかったコードを見たわけではないので、正確な原因まではわかりませんが、おそらく書き方に問題があったのではないかと思います😅

umiremixumiremix

コメントありがとうございます!
確かにこちらに関しては私の記述の問題だと思うので文章修正させていただきます🙇‍♂️