みかづきブログ・カスタム

基本的にはちょちょいのほいです。

Next.jsを使って、スマートフォン用のPull to Reflash(引っ張って更新)を実装する 📱

Next.jsを使って、スマートフォン用のPull to Reflash(引っ張って更新)を実装しました。

DEMO

https://next-pull-to-refresh.vercel.app/

※ スマートフォン限定です

ソースコード

PullToRefreshView.tsx

'use client';

import styled, { keyframes } from 'styled-components';
import { TbReload } from 'react-icons/tb';
import {
  ReactNode,
  useEffect,
  useCallback,
  useRef,
  useState,
  TouchEvent
} from 'react';

enum scrollStateType {
  start,
  move,
  end
}

export function PullToRefreshView({
  children,
  onRefresh = function() {}
}: {
  children: ReactNode;
  onRefresh?: () => void;
}) {
  const scrollYRef = useRef<number>(0);
  const scrollStateRef = useRef<scrollStateType>(scrollStateType.end);
  const [ startY, setStartY ] = useState<number>(0);
  const [ scrollY, setScrollY ] = useState<number>(scrollYRef.current);

  useEffect(() => {
    tick();
  }, []);

  const handleTouchStart = useCallback((evt: TouchEvent) => {
    setStartY(evt.touches[0].clientY);
    scrollStateRef.current = scrollStateType.start;
  }, []);

  const handleTouchMove = useCallback((evt: TouchEvent) => {
    if (evt.touches[0].clientY - startY < 0) {
      return;
    }

    const scrollRate = 10;

    scrollYRef.current = Math.max(Math.sqrt((evt.touches[0].clientY - startY) * scrollRate), 0);
    scrollStateRef.current = scrollStateType.move;
    setScrollY(scrollYRef.current);
  }, [startY]);

  const handleTouchEnd = useCallback(() => {
    const limitScrollY = 40;
    scrollStateRef.current = scrollStateType.end;

    if (limitScrollY <= scrollY) {
      onRefresh();
    }
  }, [scrollY]);

  const tick = useCallback(() => {
    if (scrollStateRef.current === scrollStateType.end) {
      const scrollRate = .9;

      scrollYRef.current = scrollYRef.current * scrollRate;

      if (Math.abs(scrollYRef.current) < 1) {
        scrollYRef.current = 0;
      }

      setScrollY(scrollYRef.current);
    }

    requestAnimationFrame(tick);
  }, [scrollY]);

  return (
    <Wrapper
      onTouchStart={ handleTouchStart }
      onTouchMove={ handleTouchMove }
      onTouchEnd={ handleTouchEnd }
    >
      <div style={{ transform: `translateY(${ scrollY }px)` }}>{ children }</div>
      <p style={{
        opacity: Math.min(scrollY, 1),
        transform: `translateY(${ Math.min(scrollY, 40) }px)`
      }}>
        <TbReload />
      </p>
    </Wrapper>
  );
}

const rotate = keyframes`
  0% {
    transform: rotate(0deg);
  }

  100% {
    transform: rotate(360deg);
  }
`;

const Wrapper = styled.div`
  position: fixed;
  top: 0; bottom: 0;
  left: 0; right: 0;
  
  > div {
    position: relative;
    width: 100%;
    height: 100%;
  }

  > p {
    position: absolute;
    top: 16px;
    left: 0; right: 0;
    font-size: 32px;
    text-align: center;
    transition: opacity .2s ease-in-out;

    svg {
      color: #424242;
      animation: ${ rotate } .8s linear infinite;
    }
  }
`;

使い方

page.tsx

import { PullToRefreshView } from '@/components/PullToRefreshView';

export default function IndexPage() {
  return (
    <PullToRefreshView>
        <div>
          <p>hogehoge...</p>
        </div>
    </PullToRefreshView>
  )
}

基本的には、引っ張って更新の対象にしたい箇所をPullToRefreshViewで囲んであげればOKです。
スマートフォン用のページで使うことを前提に作ったので、PCでは動かない点には注意が必要です。
また、現状スクロールしないページでしか使ったことがないです。

あと、最近知ったんですが、Pull to Reflashの特許はTwitter社が持っていたんですね。
Twitter社はこの特許を防御にしか使わないと発表していましたが、X社になってどうなったのかは調べても分かりませんでした。