Next.js で Page Transition を実装する

この記事はNext.js Advent Calendar 2019 16日目の記事です。

Qiitaに書くのは久々の [@_Ria0130](https://twitter.com/_Ria0130)です。

SPAを作っていても Transition を実装したことない方結構いらっしゃるんじゃないでしょうか？
ページ遷移と Transition の組み合わせって結構難しくって、クロスフェードとかをしようとすると遷移前と遷移後の要素をどちらも描画したままスタイルを適応する必要があります。

React など VirtualDOM を扱うライブラリを採用しているとDOMの反映を自動でしてくれて楽な反面、要素のコントロールが自分でできないので少し複雑です。

React で有名な Transition ライブラリだと [React Transition Group](https://reactcommunity.org/react-transition-group/) があるのですが、今回は [UIT INSID Eep.29](https://uit-inside.linecorp.com/episode/29) で紹介されていて気になっていた [react-spring](https://www.react-spring.io) を使って実装する方法を紹介していきます。

## _app.jsx
```_app.jsx
import React from "react";
import App, { Container } from "next/app";
import { PageTransition } from "../components/PageTransition";
import { GlobalStyle } from "../components/GlobalStyle";

export default class extends App {
  static async getInitialProps({ Component, ctx }) {
    let pageProps = {};

    if (Component.getInitialProps) {
      pageProps = await Component.getInitialProps(ctx);
    }

    return { pageProps };
  }

  render() {
    const { Component: SsrComponent, pageProps: ssrPageProps } = this.props;

    return (
      <Container>
        <GlobalStyle />
        <PageTransition>
          {({ Component, pageProps }) => {
            return Component ? (
              <Component {...pageProps} />
            ) : (
              <SsrComponent {...ssrPageProps} />
            );
          }}
        </PageTransition>
      </Container>
    );
  }
}
```

`_app.jsx` では次で説明する `<PageTransition>` でページのコンポーネントを囲みます。



## PageTransition.jsx
```PageTransition.jsx
import React, { useContext } from "react";
import styled from "styled-components";
import { useTransition, animated } from "react-spring";

import { withRouter } from "next/router";

const Context = React.createContext();

const Provider = ({ router, children }) => (
  <Context.Provider value={router}>{children}</Context.Provider>
);

const useRouter = () => useContext(Context);
const RouterContextProvider = withRouter(Provider);

const Transition = ({ children, ...props }) => {
  const router = useRouter();
  const transitions = useTransition(router, router => router.pathname, {
    from: { opacity: 0 },
    enter: { opacity: 1 },
    leave: {
      position: "absolute",
      top: 0,
      right: 0,
      bottom: 0,
      left: 0,
      opacity: 0
    }
  });

  return (
    <>
      {transitions.map(({ item, props: style, key }) => {
        const { Component, props } = item.components[item.pathname] || {};

        return (
          <Page key={key} style={style}>
            {children(
              item ? { Component, pageProps: props && props.pageProps } : {}
            )}
          </Page>
        );
      })}
    </>
  );
};

export const PageTransition = ({ children, ...props }) => {
  return (
    <RouterContextProvider>
      <Transition {...props}>{children}</Transition>
    </RouterContextProvider>
  );
};

const Page = styled(animated.main)`
  min-height: 100%;
  height: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
`;
```
`PageTransition.jsx` ではコンポーネントが複数あって Context も宣言してしまってるのですが、なるべく `PageTransition` を使う側で `_app.jsx` のようにシンプルになるようまとめて宣言しています。

`PageTransition` コンポーネントでは `next/router` の `withRouter` を保持する Context を宣言し、 `Transition` コンポーネントを呼び出します。
`Transition` コンポーネントでは `router` の値を `react-spring` の `useTransition` に渡すことで Transition を実装しています。

[CodeSandbox](https://codesandbox.io/s/react-spring-nextjs-routes-q80c6) に全体のソースがあるので試したい方はそちらからご確認ください。

この実装だとページ全体ではなく、コンテンツだけあるいはサイドバーだけ Transition させることも可能で、その場合は _app.jsx ではなくコンテンツのラッパーコンポーネントやサイドバーで `PageTransition` を呼び出すことで出来ると思うのでお試しください。

それでは、よいReactライフを。