お知らせ

現在サイトのリニューアル作業中のため、全体的にページの表示が乱れています。
投稿日:
ライブラリ::Next.js言語::TypeScript

nextjs.orgをサラーっと読み流したログ
Next.js 9.3時点でのドキュメント、SSRについては基本言及しない

ドキュメントの和訳でもなんでもなく単なる書き殴りです
書き殴りなので情報の漏れなどは当然ありますし、そもそも知りたいところしか読んでません。
つまり、ここに書いてあることは何も信じてはならないので、真実は君の目で確かめて欲しい。
私はまだコードも書いてないし何もわからない、知らない、関知できない!すまないッッ!!!!

Getting Started

システム要件

  • Node.js 10.13 or later
  • MacOS, Windows (including WSL), and Linux are supported

プロジェクトの生やし方

  • create-next-app --example with-typescript project-name

プロジェクトの蹴り方

  • dev - 開発サーバーが上がるよ
  • build - プロダクションビルドを生成するよ
  • start - プロダクションサーバーを上げるよ

Basic Feature

Pages

ページの生やし方
  • pages/about.tsxのようにすると静的ページが生えるよ。やったね。
  • pages/posts/[id].tsxなら動的ルーティングも可能だ!
  • コンポーネントをexportするときは面倒でもexport defaultしないと上手く行かないので注意しよう
    • これはダメ
      • export const Foo = () => { return <p /> };
    • 多分コンポーネントでなければ大丈夫?
事前レンダリング
  • Next.jsでは基本的にページを事前レンダリングするよ
  • つまり静的HTMLとして事前に吐き出すよ
    • これによってSEO効果やページのパフォーマンスが向上するよ
    • このHTMLには最小限のJSが含まれていて、ページの対話性は保たれているよ
      • この工程をhydrationと呼ぶよ
二つの事前レンダリング

Next.jsでは次の二種類のレンダリングに対応しているよ

  • Static Generation
    • 静的HTMLを吐くからSEOもパフォーマンスもバッチリ
  • Server-side Rendering
    • 毎回サーバーで捏ねるからパフォーマンスはイマイチ
    • CDNの設定も大変

勿論、CSRはどちらでも使えるのだけど、CSRのデータフェッチについてはこっち

Data Fetching

データの取得方法について

getStaticProps
  • この関数はライフサイクルフックなようなもので、Static Generationの場合、HTML生成前に実行されるよ
    • つまりサーバーサイドで事前にデータを拾っておいて詰め込む時に使う
export const getStaticProps = (context) => {
  return {
    props: {}, // will be passed to the page component as props
  };
};
  • contextの中身はここを参照
    • RouterのProps的なものが入っているっぽい?
    • 使わないのであれば省いてもいい
  • ここの戻り値はコンポーネントの引数に入ってくるよ
    • props required。突っ込みたいデータを入れてね
    • revalidate Optional。ISRをする時に使うよ。詳しくはここ
    • notFound Optional。trueにすると404が表示される。使用例は以下のような感じ
export const getStaticProps = (context) => {
  const res = await fetch(`https://.../data`);
  const data = await res.json();
  if (!data) {
    return {
      notFound: true,
    };
  }
  return {
    props: {}, // will be passed to the page component as props
  };
};

Static File Serving

  • public/配下は静的ファイルの配置場所として使える
    • 例えば次のコードでYAMLファイルの読み込みができたりする
      • fs.readFileSync('public/biography.yaml')
  • 余談ですがgetStaticProps()path.join(process.cwd(), 'contents/biography.yaml')みたいに書いてやるとpublic外のファイルを読めます

確認環境

  • historyのバージョンとreact-router-domのバージョンが上手く噛み合ってないと既存実装で型エラーが出る可能性がある
Env Ver
react 17.0.2
react-dom 17.0.2
react-router-dom 5.2.0
react-scripts 4.0.3
typescript 4.2.3
history 4.10.1

サンプルコード

  • ToPageに飛ばしたlocation.stateを検査する内容

ToPage.tsx

export const ToPage = () => {
  const location = useLocation<{ test?: string }>();
  return <p>from {location.state?.test ?? 'nothing'}</p>;
};

ToPage.spec.tsx

const renderWithRouter = (ui: JSX.Element, { route = '/' } = {}) => {
  window.history.pushState({}, 'Test page', route);

  return render(ui, { wrapper: BrowserRouter });
};

describe('onload', () => {
  it('from history.push', () => {
    const history = createMemoryHistory<{ test: string }>();
    history.push(AppRoute.to.path, { test: 'foo' });
    const { container } = renderWithRouter(
      <Router history={history}>
        <ToPage />
      </Router>
    );

    expect(container).toHaveTextContent('from foo');
    expect(history.location.state.test).toBe('foo');
  });
});
  • まずhistory.push('/path/to/dest')自体のテストを書くことはないはずなので、何かしらの副作用として実行される事が前提です
  • testing-library.com に書いてあることの応用です
/**
 * useHistory() を使えるようにするテストユーティリティ
 */
const renderWithRouter = (children: JSX.Element, { route = '/' } = {}) => {
  window.history.pushState({}, 'Test page', route);

  return render(children, {
    wrapper: BrowserRouter,
  });
};

// spy 用のモック関数
const mockPush = jest.fn();
// react-router-dom をモックにする
jest.mock('react-router-dom', () => ({
  useHistory: () => ({
    // モック関数を設定
    push: mockPush,
  }),
}));

describe('Bar', () => {
  it('history.push のテスト', () => {
    renderWithRouter(<Bar />, {
      route: '/foo/bar',
    });

    // ここらへんに同期的に副作用を起こす処理を書く
    // 以降、副作用で history.push('/foo/baz') されたとする

    // 呼ばれたことの確認
    expect(mockPush).toBeCalled();
    // 引数の確認
    expect(mockPush.mock.calls[0][0]).toBe('/foo/baz');
  });
});
投稿日:
言語::TypeScriptライブラリ::React

サンプルコード

const onSubmit = (ev: React.FormEvent<HTMLFormElement>) => {
  ev.preventDefault();
  const target = ev.target as typeof ev.target & {
    t1: { value: string };
    t2: { value: string };
  };
  console.log(target.t1.value);
  console.log(target.t2.value);
};
return (
  <div className="App">
    <form onSubmit={(ev) => onSubmit(ev)}>
      <input type="text" id="t1" />
      <input type="text" id="t2" />
      <input type="submit" />
    </form>
  </div>
);

参考

ReactでSPAを組んでいてブラウザバックしたときのフォームの入力内容が消し飛んで気になったので、ブラウザバックした時にどうなるのかというのを軽く調査した結果

確認したパターンとしては次の2つ

  1. フォームの入力値をDOMに保持させるステートレスな方式
  2. フォームの入力値をJSに保持させるステートフルな方式

LocalStorageに画面情報を保存して復元するとか、条件分岐を使ってDOMを隠して保持させるとか、そういう系は考慮しない

結果

DOM 保持 JS 保持
SPA 内ブラウザバック 消える 保持される
SPA 外ブラウザバック 保持される 消える
コード記述量 少ない 多い

1. フォームの入力値をDOMに保持させるステートレスな方式

この方式だとhistory.push()でDOMが消し飛ぶのでSPA内でのブラウザバックで入力したデータが保持されません
その代わりSPAの外、別のサイトに遷移してからブラウザバックしたときは、DOMが残っているので入力したデータが保持されます

2. フォームの入力値をJSに保持させるステートフルな方式

この方式だとJSでステート管理をしているため、history.push()で遷移してDOMが消し飛んでも、 defaultValueに持ってる値を突っ込んであげれば、取り敢えず入力したデータを保持することが出来ます
反対にSPAの外、別のサイトに遷移してからブラウザバックしたときは、メモリの中身が飛んでるので入力したデータが保持されません

各手法の実装方式

ちゃちゃっと書いたのでコードは超雑です

確認環境

Env Ver
react 17.0.2
react-dom 17.0.2
react-router-dom 5.2.0

1. フォームの入力値をDOMに保持させるステートレスな方式

大正義ステートレスです
Function Componentはステートレスなので、こうあってほしいですよね~
コードもシンプルで管理しやすいのが素敵なところです

export const StatelessPage = () => {
  const history = useHistory();
  return (
    <div>
      <form>
        <input type="text" />
        <button onClick={() => history.push(AppRoute.dom2.path)}>submit</button>
      </form>
    </div>
  );
};

2. フォームの入力値をJSに保持させるステートフルな方式

ギルティなステートフル方式です
Class Componentを捨てたはずなのにどうしてこうなった…
コードが煩雑で管理が大変です

export const StatefullPage = () => {
  const ctx = useContext(ExampleContext);
  const history = useHistory();
  const onChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
    ctx.text = ev.target.value;
  };

  return (
    <div>
      <form>
        <input
          type="text"
          defaultValue={ctx.text}
          onChange={(ev) => onChange(ev)}
        />
        <button onClick={() => history.push(AppRoute.ctx2.path)}>submit</button>
      </form>
    </div>
  );
};