こんにちは。エンジニアの松浦です。
私は現在、フロントエンドエンジニアとしてメディアリストの機能実装やバグ修正を行っています。今回は新UIのメディアリストがリリースされて半年経ち、今までに出てきたフロントエンドのバグやどのようなものを修正してきたかについて書いていこうと思います。
メディアリストとは
メディアリストは、プレスリリースをどのメディアに配信するか取りまとめたリストのことです。企業のユーザーはあらかじめプレスリリースを送りたいメディアを選び、それをリストとして管理することができます。
そんなメディアリストですが、2024年12月にUIの大幅なアップデートをしました。詳細については下記のプレスリリースからどうぞ。
新着メディア表示機能とメディア検索UI刷新。PR TIMESメディアリスト機能をリニューアル
改善したバグ事例と対応方法
今回バグ修正した箇所は主に2箇所です。そのバグでシステムが壊れてしまうなどのクリティカルなものではないですが、UI/UX上使いにくくなってしまうものだったのでなるべく早急に対応する必要があったものをご紹介したいと思います。
メディアリスト結合時、メディアリスト名が残り続けてしまう
メディアリストの機能として、メディアリスト同士を結合するといったものがあります。この機能は、複数のメディアリストをまとめて一つにしたい時に使われる機能です。 メディアリスト同士を結合した際、メディアの総数が上限300件を超えた場合はメディアを選択して削除できるドロワーが表示されます。

ドロワー内では結合後のメディアリスト名を入力することができます。このバグでは、そのメディアリスト名が内部で残り続けてしまい、次回ドロワーが開いてメディアリスト名を変更せずに保存ボタンをクリックすると重複エラーになってしまうといったものでした。
こちらの原因は、useStateがドロワーが閉じられても破棄されない階層にあり、useStateの値が保持され続けてしまうためによるものでした。なので修正としては、ドロワーが閉じられた際useStateの値が破棄されるようなコンポーネントに移動することによって修正することができました。
下記が改善前と改善後のコードです。
// Before
type Props = {
readonly isOpen: boolean;
readonly onClose: () => void;
readonly onOpenChange: (open: boolean) => void;
}
function MediaListJoinDrawer({
isOpen,
onClose,
onOpenChange,
}: Props) {
const [mediaListTitle, setMediaListTitle] = useState('');
// ドロワーが閉じられた際中身は破棄されるが、
// mediaListTitleはこのコンポーネントにあるため破棄されない。
return (
<MediaListDrawer
isOpen={isOpen}
onOpenChange={onOpenChange}
onClose={onClose}
>
<div>
<h2>{mediaListTitle}</h2>
<input
type="text"
onChange={(event) => {
setMediaListTitle(event.target.value)
}
}>
{/* ... */}
</div>
</MediaListDrawer>
);
}// After
type Props = {
readonly isOpen: boolean;
readonly onClose: () => void;
readonly onOpenChange: (open: boolean) => void;
};
function MediaListJoinDrawer({isOpen, onClose, onOpenChange}: Props) {
return (
<MediaListDrawer
isOpen={isOpen}
onOpenChange={onOpenChange}
onClose={onClose}
>
<MediaListJoinDrawerContainer />
</MediaListDrawer>
);
}
function MediaListJoinDrawerContainer() {
const [mediaListTitle, setMediaListTitle] = useState('');
// MediaListDrawerの入れ子なので、ドロワーが閉じられた際破棄される。
return (
<div>
<h2>{mediaListTitle}</h2>
<input
type='text'
onChange={(event) => {
setMediaListTitle(event.target.value);
}}
/>
{/* ... */}
</div>
);
}
履歴復元後に編集ドロワーの絞り込みがリセットできていない
メディアリストには、メディアリストの編集履歴から復元する機能があります。復元ドロワーが閉じた後、編集ドロワーのメディア種別・検索ワード・発行部数/PVのフィルターをリセットできていないといったものがありました。

この問題は、編集ドロワーと復元ドロワーの親コンポーネントにそれらのフィルター状態を持たせ、propsとして渡すことで解決できます。しかし、この場合は修正範囲が広くなり、propsのバケツリレーも増えてしまうという課題もありました。
そこでkeyを使用することでデメリットを抑えつつ、再レンダリングさせるコンポーネントを最小限にすることができました。
下記がkeyを導入したコードです。
function MediaListPage() {
const [mediaListTableKeyFlag, setMediaListTableKeyFlag] = useState(false);
return (
<div>
{/* ... */}
<MediaListEditDrawer
mediaListTableKey={mediaListTableKeyFlag ? 'key1' : 'key2'}
/>
<MediaListEditDrawer
onRestore={() => {
// 復元ボタンがクリックされた時、keyを切り替える
setMediaListTableKey((previous) => !previous);
}}
/>
</div>
);
}
function MediaListEditDrawer({
mediaListTableKey,
}: {
readonly mediaListTableKey: string;
}) {
return (
<MediaListDrawer>
{/* ... */}
<div>
<MediaListTable key={mediaListTableKey} />
</div>
</MediaListDrawer>
);
}
最後に
これらのバグ修正をしていてレンダリングのタイミングの違いやuseStateの挙動、keyプロパティの活用について学ぶことができました。今回のバグはいずれも致命的なものではありませんが、細かな不具合がユーザーの使いやすさやサービスの安定性に大きく影響することもあるため、これからもさらにメディアリストをブラッシュアップしていこうと思っています。

