- 投稿日:
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>
);
};
- 投稿日:
substring
確認環境
Env | Ver |
---|---|
Jinja | 2.11.0 |
Python | 3.8.5 |
サンプルコード
- 変数名[begin:end]で指定する
{% set some_variable = string_variable[0:100] %}
ループ処理で変数代入を行う
確認環境
Env | Ver |
---|---|
Jinja | 2.11.0 |
Python | 3.8.5 |
- Jinja2でループ処理の中で変数の足しこみとかをするやつ
- 代入先の変数宣言は
{% set ns = namespace(title = "") %}
のようにしてやる必要がある - 後はループの中で
set
してやれば上手くいく - 変数は宣言したブロックがスコープになるので、スコープを広げたいときは適当にブロックを上げてやると良い
- 代入先の変数宣言は
サンプルコード
- MkDocsのテンプレートでパンくずリストを生成するコード
[A] [B] [C]
みたいな感じ
{% set ns = namespace(title = "") %}
{% for doc in page.ancestors %}
{% set ns.title = "[" + doc.title + "] " + ns.title %}
{% endfor %}
テンプレートファイルを読み込む
確認環境
Env | Ver |
---|---|
Jinja | 2.11.0 |
Python | 3.8.5 |
サンプルコード
- フォルダ構成
─src
├─app
│ ├─configs
│ │ └config.yaml
│ ├─templates
│ │ └─main.html
│ └─__init__.py
└─main.py
- コード
from jinja2 import Environment, PackageLoader, select_autoescape
env = Environment(
# appはフォルダ構成のappフォルダを指す
loader=PackageLoader('app', 'templates'),
autoescape=select_autoescape(['html', 'xml'])
)
template = env.get_template('main.html')
# `render()` の引数は埋め込み変数を KeyValue 形式で指定
print(template.render(the='variables', go='here'))
yamlファイルから設定をロードする
確認環境
Env | Ver |
---|---|
Jinja | 2.11.0 |
Python | 3.8.5 |
サンプルコード
- フォルダ構成
─src
├─app
│ ├─configs
│ │ └config.yaml
│ ├─templates
│ │ └─main.html
│ └─__init__.py
└─main.py
- コード
from yaml import safe_load as yamlLoad
from jinja2 import Environment, PackageLoader, select_autoescape
env = Environment(
loader=PackageLoader('app', 'templates'),
autoescape=select_autoescape(['html', 'xml'])
)
template = env.get_template('main.html')
with open('app/configs/config.yaml') as fYaml:
print(template.render(yamlLoad(fYaml)))
- 投稿日:
概要
- Jinja2で作られている
- Python向けのテンプレートエンジン
- これを理解することで自在にカスタマイズできる。たぶん
- テンプレート変数について、まともなリファレンスはないので、mkdocsのGitHubリポジトリを眺めて知るのが早い
- www.mkdocs.org にも一応書いてあるが、網羅されていない
カスタマイズ方法
- まずはMaterial for MkDocsを読んでリファレンスを理解する
- 設定で解決する内容ならここで終わり
- テンプレートのカスタマイズが必要な場合、
overrides/main.html
を生やしてブロック単位でいじる- 元のブロックはgithub.comを参考にする
- 構文はjinja.palletsprojects.com
- 投稿日:
フックを単体でテストするケースを想定。
このパターンはコンポーネントからフックを切り離しているケースで有用。手法としては@testing-library/react-hooks
のrenderHook()
を使う。
例
カスタムフック
export const useUserForm = () => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const onChangeUserName = (ev: string) => {
setUsername(ev);
};
const onChangePassword = (ev: string) => {
setPassword(ev);
};
return {
username,
password,
onChangeUserName,
onChangePassword,
};
};
テストコード
it('onChangeUserName で username が設定されること', () => {
// `renderHook` で Hook をレンダリング
const { result } = renderHook(() => useUserForm());
// `act()` で Hook のイベントを叩く
act(() => result.current.onChangeUserName('foo'));
// 結果を見る
expect(result.current.username).toBe('foo');
});
- 投稿日:
TSの情報に乏しく無駄にハマったのでメモ程度に
確認環境
Env | Ver |
---|---|
React | 17.0.1 |
TypeScript | 4.1.3 |
サンプルコード
- 今回はサンプルとして
<input />
をラップしたコンポーネントのフォーカスを変更するためにref
を使います
Child.tsx
import { forwardRef } from 'react';
export type ChildProps = {
type: 'text' | 'password' | 'number';
onChange(changeValue: string): void;
};
// function 記法でないと ESLint が怒るので無効化
// eslint-disable-next-line react/display-name
export const Child = forwardRef<HTMLInputElement, ChildProps>(
// このpropsに明示的な型定義がないと型エラーが出る
(props: ChildProps, ref) => {
const onChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
props.onChange(ev.target.value);
};
return (
<input
ref={ref}
type={props.type}
onChange={(ev) => onChange(ev)}
/>
);
}
);
Parent.tsx
- 下側の
<Child />
だけフォーカスが行くようにしてます
import { useEffect, useRef } from 'react';
import { Child } from './Child';
export const Parent = () => {
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
ref?.current?.focus();
}, []);
return (
<ul>
<li>
<Child
type={'text'}
onChange={(ev) => console.log('top input', ev)}
/>
</li>
<li>
<Child
ref={ref}
type={'text'}
onChange={(ev) => console.log('bottom input', ev)}
/>
</li>
</ul>
);
};