0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

`const YEAR = 2026` は時限爆弾でした(年明けに全データが画面から消えるアプリ)

0
Posted at

この記事の要点(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周見るのが早いです。

まとめ

  • 年・月・年度をコードに直接書かない。 動くけれど、動く期限があります
  • 直すときは定数名だけでなく、画面に出る文字列でも検索する
  • 年を切り替えられるようにしたら、**「切り替えた状態で書き込みが起きたらどうなるか」**を必ず確認する
  • 選択中の年が消えるケース(記録の全削除)にフォールバックを用意する

「動いているから大丈夫」と「壊れる日が決まっている」は両立します。個人開発だとテストも薄いので、こういうものはリリース前にコードを読み返して見つけるしかないな、と思った次第です。

アプリはこれです → 推し課金メーター。ほかに作ったものは sesebox にまとめています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?