- 投稿日:
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')
- 例えば次のコードで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');
});
});
- 投稿日:
サンプルコード
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つ
- フォームの入力値をDOMに保持させるステートレスな方式
- フォームの入力値を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>
);
};