- 投稿日:
条件分岐でコンポーネントの出し分けをしている時に正しくコンポーネントが出ているかどうかに使えるやつ
UIロジックのリグレッションテストで使える
分岐結果の出力を見てるだけなのでテストとして壊れづらく、運用しやすいと考えている
確認環境
Next.jsで確認してるけど素のReactでも同じだと思う
Env | Ver |
---|---|
@swc/core | 1.2.133 |
@swc/jest | 0.2.17 |
jest | 27.4.7 |
next | 12.0.8 |
react | 17.0.2 |
react-dom | 17.0.2 |
react-test-renderer | 17.0.2 |
typescript | 4.5.4 |
テスト対象
テストのためにコンポーネントを細かくexport
すると名前空間が汚染されるのが悩み…
type BaseProps = {
id: string;
};
type SwitchExampleProps = BaseProps & {
display: 'Foo' | 'Bar';
};
export const Foo = (props: BaseProps) => {
return (
<div id={props.id}>
<p>Foo</p>
</div>
);
};
export const Bar = (props: BaseProps) => {
return (
<div id={props.id}>
<p>Bar</p>
</div>
);
};
export const SwitchExample = (props: SwitchExampleProps) => {
if (props.display === 'Foo') {
return <Foo id={props.id} />;
} else {
return <Bar id={props.id} />;
}
};
テストコード
react-testing-libraryの.toHaveAttribute()
や.toHaveDisplayValue()
を書き連ねるより圧倒的に楽で保守性も良いと思う
import TestRenderer from 'react-test-renderer';
import { Bar, Foo, SwitchExample } from './SwitchExample';
type TestCase = {
name: string;
param: Parameters<typeof SwitchExample>[0];
actual: JSX.Element;
};
describe('SwitchExample', () => {
const testCaseItems: TestCase[] = [
{
name: 'Foo',
param: {
id: 'hoge',
display: 'Foo',
},
actual: <Foo id={'hoge'} />,
},
{
name: 'Bar',
param: {
id: 'piyo',
display: 'Bar',
},
actual: <Bar id={'piyo'} />,
},
];
testCaseItems.forEach((item) => {
// eslint-disable-next-line jest/valid-title
it(`switched condition ${item.name}`, () => {
const result = TestRenderer.create(
<SwitchExample id={item.param.id} display={item.param.display} />
);
const actual = TestRenderer.create(<>{item.actual}</>);
expect(result.toJSON()).toStrictEqual(actual.toJSON());
});
});
});
参考記事
- 投稿日:
GitHub Actionsで前のジョブの結果を後続で利用したい時に使えるやつ
サンプルコード
- 適当な文字列を変数にセットして、各ジョブで出力する例
name: outputs sharing example
on:
pull_request:
jobs:
first:
runs-on: ubuntu-latest
outputs:
# <out-job-name>: ${{ steps.<step-id>.outputs.<in-job-name> }}
baz: ${{ steps.foo.outputs.bar }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- id: foo
# この場合、FOO-BARという値がセットされる
run: echo "::set-output name=bar::$(echo FOO-BAR)"
- id: test
# steps.<step-id>.outputs.<in-job-name>
run: echo "${{ steps.foo.outputs.bar }}"
second:
needs: [first]
runs-on: ubuntu-latest
steps:
- id: test
# needs.<job-id>.outputs.<out-job-name>
run: echo "${{ needs.first.outputs.baz }}"
参考
- 投稿日:
これは、かつて参画したGitHubを利用したプロジェクトでブランチフローが悪く事故が多発したので考案し、運用した内容です。
基本的には後々の運用を考えた時に情報源になり、かつGit操作に極力手間を取られることがなく、CI/CDを回しながら品質を維持できる内容で考えています。
フローの要件として考えたこと
取り回しが単純明快であること
開発以外の要素に振り回されないように単純なフローにしていて、世に言われる履歴の綺麗さとか言うのは個人的には関心が薄いので重視していません。その代わりコミットが壊れないことや、インデックスとして見やすくなるような部分を重視しています。
GitHubとの相性が良いこと
まず個々の開発タスクをIssueベースで管理し、Pull Requestベースで取り込む運用としました。
Pull Requestの取り込み方式はSquash Mergeとし、メインブランチのコミット履歴がPull Requestのマージコミットになるようにしました。
これはメインブランチのコミット履歴はPull Requestのマージコミットだけあれば後から追えるというのと、メインブランチのコミットログを単純にする意味でこの方式にしています。
CI/CDを活用しやすいこと
これは割とどこでもやっていると思いますが、ブランチ名にdevelop
とかstaging
, production
とか付けて管理することで、自動的に環境を識別できるようにしました。
実際に運用したフロー
フロー図
フローの運用内容
develop/main
ブランチを最新ブランチとする運用develop/main
ブランチ相当のものが複数ある状態というのが世の中にあると思いますが、管理が非常に大変なのでそれはしない方向にしました
- 機能ブランチを
develop/main
ブランチに取り込むのはSquash Merge- 基本的に変更履歴を見る時はGitLensや
blame
で変更行からPull Requestを当てて、そこを見に行くという運用にしていました
- 基本的に変更履歴を見る時はGitLensや
- 機能ブランチに
develop/main
ブランチを取り込むのはmerge
- 一般的には
rebase
が多いと思いますが、次の観点から採用しませんでした- どのポイントで取り込んだのかわからない
merge
と比較した場合にコンフリクト対応に手を取られる- 経験上ここで事故が頻発する
- 素直に
push
出来ない
merge
であれば以下のように単純な流れに出来ますし、push前に差分確認して事故を防ぐことも容易ですgit switch develop/main
git pull
git switch -
git merge -
- コンフリクトがあれば解消
git push
- 一般的には
- デプロイ方式によるルートブランチ分割
- ルートブランチ名によってデプロイ先を変更できるようにしました
- GitHub Actionsでブランチ名を拾って環境変数を差し替えることでデプロイするワークフローを組んでいます
staging/
やproduction/
ブランチはdevelop/main
からcheckout
する運用です- これらはデプロイするためだけの使い捨てブランチなので毎回生えます
あとがき
このフローの利点としてはマージでコンフリクトが起きても基本的にCurrentとIncomingを一回比較するだけで済むのでコンフリクトの解消が簡単で、コンフリクト時はIncomingが壊れないようにCurrentを直すのが基本になり、Currentを優先する場合は適宜上書きするといった内容です。
ブランチの合流はmerge
だとマージコミット分一回の解消だけでいいので事故の発生要因が低いのがrebase
と比較した時の利点です。rebase
だとコミット回数分再帰的に合流させる必要があるのでブランチの寿命が長かったりすると苦行になってきます。
このフローができた経緯としては元々はGitHub Flowをベースにしていたのですが、色々やっていくうちにこうしたらもっと良くなるのではないか?というのを試行錯誤していてこの形に落ち着いたのですが、後から調べたらGitLab Flowに近い形式に見えたので、似た内容は既に誰かが考えているものだなと感じました。
上で挙げた内容の他にもGitHubのリポジトリ設定でブランチ保護のルールを設定したり、Pull Requestのマージ設定でSquash mergingだけ許可したり、ヘッドブランチの自動削除をするなど、基本的に面倒なことを考えたり、しなくて良い様にするなど、開発に注力しやすいように環境を整えると心理的な抵抗が少なく運用できて便利だなと感じています。
- 投稿日:
別にReactに限った話でもないのですが、実務で悩ましいコードにあたって頭を抱えてる内にハゲてきたので、ハゲがこれ以上進まない事を祈り書きました。いやいっそのこと全ハゲになりたいですが、それはさておき
とりあえず個人的には以下2点を意識すれば起きないとは思うので、Next.jsを使った簡単なサンプルを交えながら3例ほどケーススタディ形式で紹介していきます
- 密結合にしない
- シンプルに実装する
挙げている事例については例示用にフルスクラッチで書いています(主題と関係ないコードは端折ってます
ここはこうした方がより良いのでは?などのご意見があれば是非コメントいただけると嬉しいです
コンポーネントのOptional引数のオーバーライド
コンポーネント引数をコンポーネント側で書き換えるような実装はどうかなと思います
問題点
- 親の預かり知らぬところで値が設定されている
- 誰かが知らずに設定値をすり替えたりしたら不具合が起きそうです
- 型のコメントと設定値が異なる
- 型のコメントと実装が異なるので、誰かが直すかもしれません
- すると、コンポーネントを参照している全コンポーネントに影響が波及します
- そもそも型と実装は本質的に関係ないので、こういった運用はNG
- 型のコメントと実装が異なるので、誰かが直すかもしれません
一例
type AccountContainerProps = {
email: string;
// デフォルトtrue
isUpdate?: boolean;
// デフォルトfalse
gotNotify?: boolean;
};
export const AccountContainer = ({
isUpdate = true,
gotNotify = true,
...props
}: AccountContainerProps) => {
...
}
改善案
呼び元で明示的に指定するように変更しています
これによって親は子の実装を知る必要がなくなり、責務がそこで閉じるようになりました
改善点
- 親の預かり知らぬところで値が設定されなくなった
- これで子コンポーネントで値が変わることに怯える必要はなくなりました
- 型の初期値コメントを削除できた
- 実装と一致する保証がないコメントは削除するべきでしょう
type AccountContainerProps = {
email: string;
isUpdate: boolean;
gotNotify: boolean;
};
export const AccountContainer = (props: AccountContainerProps) => {
...
}
コンポーネントのOptional引数の多重オーバーライド
コンポーネントのOptional引数のオーバーライドが多重化されている上に、なんか途中で更に書き換えられているとかいう地獄
どうしてそんなことをするのか…
一例
type AccountTemplateProps = {
email: string;
// デフォルトtrue
isUpdate?: boolean;
// デフォルトfalse
gotNotify?: boolean;
from: 'register' | 'update';
};
export const AccountTemplate = ({
isUpdate = false,
gotNotify = true,
...props
}: AccountTemplateProps) => {
if (props.from === 'register') isUpdate = false;
return <AccountContainer {...props} />;
};
type AccountContainerProps = {
email: string;
// デフォルトtrue
isUpdate?: boolean;
// デフォルトfalse
gotNotify?: boolean;
};
export const AccountContainer = ({
isUpdate = true,
gotNotify = true,
...props
}: AccountContainerProps) => {
...
}
改善案
前項のように明示的に値を渡してあげるようにしましょう
直列に分散されたコンポーネント
コンポーネントの中にコンポーネントがネストされ続けてるパターンです
一見して何をしているのか分かりづらい上、StateやらHookやら色んな処理が各コンポーネントに分散配置されていることもあります
問題点
- 親からみると子が何をしているのかが分かりづらい
- コンポーネント名が意味をなしていない(責務が別れていない)
- 子コンポーネントが状態を持っているため、親コンポーネントでハンドリングができない
- 子コンポーネントのロジック変更が参照している全コンポーネントのロジックに波及する
一例
親コンポーネント
RegisterHeader
なる物が差し込まれていることだけがわかります
このコンポーネントが何をするのかはパッと見ではよくわかりません
const AccountUpdatePage = () => {
return <Register header={<RegisterHeader />} />;
};
ラッピングしているコンポーネント
ラッパーなのでこのコンポーネントそのものは何をしているのかわかりません
type RegisterProps = {
header: JSX.Element;
};
export const Register = (props: RegisterProps) => {
return <>{props.header}</>;
};
差し込まれているコンポーネントの中身
どうやらRegisterHeader
はRegisterContent
を含むようです
なんでヘッダーの中にコンテンツがあるのでしょうか…
export const RegisterHeader = () => {
return (
<>
<p>head</p>
<RegisterContent />
</>
);
};
差し込まれているコンポーネントの子
現在のパスに応じて叩くAPIを変えるような実装がされていますが、もしパスが変わったり増えたりしたらこのコンポーネントの実装を知らない限り面倒なことになります
export const RegisterContent = () => {
const rt = useRouter();
const currentPath = rt.pathname;
const url =
currentPath === 'register'
? 'https://example.com/kaiin/touroku'
: 'https://example.com/kaiin/koshin';
const [username, setUsername] = useState<string | undefined>(undefined);
const onSubmit = (ev: React.FormEvent<HTMLFormElement>) => {
ev.preventDefault();
axios.post(url, {
username,
});
};
return (
<>
<form onSubmit={onSubmit}>
<input
type="text"
value={username}
onChange={(ev) => setUsername(ev.target.value)}
/>
<button>送信</button>
</form>
<RegisterFooter />
</>
);
};
export const RegisterFooter = () => {
return <p>foot</p>;
};
改善案
改善点
- 親から子への見通しが改善された
- コンポーネント名が名前の通りの意味を持つようになった
- ヘッダーはヘッダー、フォームはフォーム、フッターはフッターの責務だけに集中できます
Register
とか言う謎コンポーネントも姿を消しました
- 状態をすべて親に集約した
- 子コンポーネントのロジック変更が起きる確率が減った
- 子コンポーネント側のロジックを減らし、親から渡されるコールバックで行うようにしたため、子コンポーネントのロジック変更が他に影響する確率が減りました
親コンポーネント
- ひとまずヘッダーと入力フォーム、フッターがあるんだなという事が解るようにはなったと思います
- 状態を親に集約したのでAPI叩く時のURIも態々パスから判断しなくて良くなったのでコードの複雑性が減っています
const usePageState = () => {
const [username, setUsername] = useState('');
return {
username,
setUsername,
};
};
const onSubmit = (username: string) => {
axios.post('https://example.com/kaiin/koshin', {
username,
});
};
const AccountUpdatePage = () => {
const ps = usePageState();
return (
<>
<RegisterHeader />
<RegisterForm
onChangeUsername={ps.setUsername}
username={ps.username}
onSubmit={() => onSubmit(ps.username)}
/>
<RegisterFooter />
</>
);
};
ヘッダー
export const RegisterHeader = () => {
return <p>head</p>;
};
入力フォーム
type RegisterFormProps = {
username: string;
onChangeUsername: (value: string) => void;
onSubmit: () => void;
};
const onChangeUsername = (
ev: React.ChangeEvent<HTMLInputElement>,
setState: (value: string) => void
) => {
const value = ev.target.value;
setState(value);
};
const onSubmit = (
ev: React.FormEvent<HTMLFormElement>,
onSubmit: () => void
) => {
ev.preventDefault();
onSubmit();
};
export const RegisterForm = (props: RegisterFormProps) => {
return (
<>
<form onSubmit={(ev) => onSubmit(ev, props.onSubmit)}>
<input
type="text"
value={props.username}
onChange={(ev) => onChangeUsername(ev, props.onChangeUsername)}
/>
<button>送信</button>
</form>
</>
);
};
フッター
export const RegisterFooter = () => {
return <p>foot</p>;
};
- 投稿日:
Issueの中にあるTasksの進捗状況集計したい。したくない?したいですよね!
実はGitHubのREST APIを叩いてもTasks情報を素直に取れません
仕方がないので雑に取得するためのシェルスクリプトを書きました
自作するまでの経緯
GH CLI では出来ない
GitHub CLI使ったら簡単に出来るやろ、そう思っていた時代が私にもありました
なんとできません
検索クエリ投げないとProjectで絞れないようなかったり、痒いところに手が届かない子ですし、まぁ許しましょう
REST APIでもできない
ちょっと待ってほしい、画面上表示されてるのにAPIとしては提供されてないの?
そんなのある?
そんなデータを提供している機能はない
2016年には問題提起されていたようですが絶賛放置中
仕方ないので自作した
API叩いてIssue本文からTasksの情報を雑に抜いて集計するだけのやつです
超雑なのでインデントとかコードフェンスの中とか何も考慮してません