この記事の要点(TL;DR)
- 自作アプリに
const YEAR = 2026;と書いてありました。 - 集計もシェア画像も一覧も、全部この定数を見ていました。
- つまり2027年1月1日になった瞬間、記録が全部画面から消えて ¥0 になる状態でした。
- データ自体は消えません。でもユーザーから見れば「消えた」と同じです。
- ストアに出す前に気づけたので、その直し方と、直すときに出てきた副作用を書きます。
個人開発で 推し課金メーター という、推しに使った金額を記録するアプリを作っています。「2026年、推しに注いだ愛:¥○○」という感じで、1年ぶんを集計して見せるアプリです。
何が埋まっていたか
コードの上のほうに、こう書いてありました。
const YEAR = 2026; // 「2026年、推しに注いだ愛」— 現時点の年度スコープ
そして集計関数は、こう呼ばれていました。
const stats = statsFor(oshi, recs, YEAR);
statsFor の中身はこうです。その年の記録だけを抜き出して合計する、素直な実装です。
const recs = allRecs.filter(
(r) => r.oshiId === oshi.id && new Date(r.ts).getFullYear() === year
);
ここまで読むと分かります。2027年になっても YEAR は 2026 のままなので、
- 2027年に記録した金額は、フィルタに引っかからず画面に出ません
- 一方で2026年の記録は表示され続けます
さらに悪いことに、この定数は集計だけでなくシェア画像・推し一覧・ファイル名からも参照されていました。1か所の数字が、アプリのあちこちに効いていたわけです。
もっと厄介だったのは「文字列」のほう
定数だけなら grep YEAR で全部見つかります。ところが画面に出る文字も直接書かれていました。
<div>2026年、推しに注いだ愛</div>
<div>2026年のサマリー</div>
<div>2026年</div> {/* シェアカード */}
YEAR で検索しても、これは1件も引っかかりません。私は定数を直した後に grep 2026年 をやって、はじめて3か所の存在に気づきました。
教訓:日付や年を後から動的にするときは、定数名だけでなく「見えている文字列」でも検索する。当たり前のようでいて、変数化した気になって見落としがちです。
直し方
まず「今年」を関数にして、状態として持ちます。
function thisYear(): number {
return new Date().getFullYear();
}
// 表示している年。初期値は今年。過去の年に切り替えて見られる
const [year, setYear] = useState<number>(() => thisYear());
useState(new Date().getFullYear()) と書かず、関数を渡している点だけ注意です。前者は再レンダリングのたびに new Date() が走ります(結果は同じですが無駄です)。
次に「切り替えられる年」を作ります。記録がある年に、今年を足したものです。
const years = useMemo(() => {
const set = new Set<number>(recs.map((r) => new Date(r.ts).getFullYear()));
set.add(thisYear()); // 記録が1件も無い年でも「今年」は必ず選べるようにする
return [...set].sort((a, b) => a - b);
}, [recs]);
// 選んでいた年の記録が全部消えた場合の保険
const activeYear = years.includes(year) ? year : years[years.length - 1];
activeYear を挟んでいるのは、選択中の年の記録をユーザーが全部削除することがあるからです。そのまま year を使うと、存在しない年を見つめ続ける画面になります。
あとは YEAR を使っていた場所を全部 activeYear に置き換えます。
直したら出てきた副作用
1. 過去の年を見ながら記録すると、増えない
年を切り替えられるようにした結果、2025年を表示したまま「記録する」を押せるようになりました。記録は当然「今日」=今年に入るので、画面上は何も増えません。
ユーザーから見ると「記録したのに反映されないバグ」です。実際には正しく保存されているのに、です。
なので、記録したら今年に戻すようにしました。
setRecs((r) => [rec, ...r]);
setYear(thisYear()); // 記録は「今日」に入るので、今年に戻して結果を見せる
年を切り替えられるUIを足すときは、「切り替えた状態で書き込みが起きたらどうなるか」を必ず考える、という話です。
2. 切り替えUIが、使い始めの人には邪魔
‹ 2026年 › という切り替えを常に出すと、使い始めた人には切り替える先がありません。押せないボタンが並ぶだけです。
なので、選べる年が2つ以上あるときだけ出すようにしました。
const yearSwitcher = years.length > 1 ? ( /* ... */ ) : null;
3. 「今年の目標額」というラベルが嘘になる
目標額は年ごとではなく推しごとの設定でした。年を切り替えても同じ値です。なのにラベルが「今年の目標額」だと、過去の年を見ているときに嘘になります。「1年の目標額」に変えました。
年を動的にすると、周りの文言も連鎖して怪しくなります。 ここは grep では見つからないので、実際に年を切り替えながら画面を1周見るのが早いです。
まとめ
- 年・月・年度をコードに直接書かない。 動くけれど、動く期限があります
- 直すときは定数名だけでなく、画面に出る文字列でも検索する
- 年を切り替えられるようにしたら、**「切り替えた状態で書き込みが起きたらどうなるか」**を必ず確認する
- 選択中の年が消えるケース(記録の全削除)にフォールバックを用意する
「動いているから大丈夫」と「壊れる日が決まっている」は両立します。個人開発だとテストも薄いので、こういうものはリリース前にコードを読み返して見つけるしかないな、と思った次第です。