木曜日, 5月 1, 2025
ホームニューステックニュースTanStack Form と React Hook Form の Dirty 判定の挙動を比較してみた

TanStack Form と React Hook Form の Dirty 判定の挙動を比較してみた



React Hook Form は多くの現場で定番のフォームライブラリですが、最近は TanStack Form v1 のリリースによって、フォーム実装の選択肢がさらに広がっているように思います。

突然ですが、実際の開発では「フォームのどのフィールドが変更されたか」を正確に知りたい場面がよくあります。そこで本記事では、TanStack Form と React Hook Form の「Dirty 判定」の挙動の違いについて整理していきます。

たとえば規模が大きいプロジェクト(フィールド数が多いフォーム)では、「変更されたフィールドだけを更新したい」といった要件が存在します。具体的には、変更されたフィールドだけを保存して DB 負荷を抑えるといったケースです。

ただし、両ライブラリごとにDirty(変更済み)フィールドの判定方法や考え方が異なります。この違いを理解せずにライブラリを選ぶと、思わぬ落とし穴にはまることもあるので注意が必要です。

まずは両者の違いをざっくり比較してみます。

ライブラリ Dirty 判定の考え方
TanStack Form 「一度でも編集されたら isDirty = true」  →  元の値に戻しても true のまま
React Hook Form 「現在値 ≠ 初期値」で Dirty を判定 →  元の値に戻せば false

このように、TanStack Form は「一度でも編集されたら isDirty = true」という履歴型の判定を採用しています。

ただし、TanStack Form のコミュニティからは「現在値 ≠ 初期値」のような差分型の判定も求められており、そのため今後は「isDefaultValue」の導入が進められています。これにより、将来的には履歴型と差分型の両方のアプローチを柔軟に使い分けられるようになる予定です。こちらに関しては後述します。

なぜ TanStack Form は React Hook Form と異なる Dirty 判定を採用したのか?

TanStack Form が後発のライブラリでありながら、React Hook Form とは異なる「履歴型」の Dirty 判定を採用しているのには、いくつか理由があると考えられます。

まず、ユーザーが実際にフィールドを操作したかどうかを明確に追跡できる点が挙げられます。フォームでは「値が変わったか」だけでなく、「ユーザーがそのフィールドに触れたかどうか」という情報も重要です。特にバリデーションや UX 設計の観点から、「ユーザーが操作した」という事実を保持しておきたい場面が多くあります。

また、技術的な観点では、値の深い比較を常に行う必要がなく、「操作があったかどうか」だけを記録するシンプルな実装になっているため、特に大規模なフォームでパフォーマンス面のメリットがあると考えられます。

そのため、React Hook Form のような「現在値 ≠ 初期値」の判定を採用していると、フォームの履歴管理が複雑になり、パフォーマンスの低下やバグの発生を招く可能性があります。


[参考]

既に出てきていますが、改めて本記事ではフォームの「Dirty(変更済み)」判定の考え方として、以下の 2 つの型を定義します。

履歴型(TanStack Form の isDirty

  • 一度でもフィールドを編集したら、その後元の値に戻しても「変更あり」と判定します。
  • 「ユーザーがこのフィールドを操作したかどうか」を記録するイメージです。
  • たとえば、名前を「山田」→「佐藤」→「山田」と変更した場合、最終的な値は初期値と同じでも dirty=true となります。

差分型(React Hook Form の isDirty

  • 現在の値が初期値と異なる場合のみ「変更あり」と判定します。
  • 「今この瞬間、初期値とどこが違うか」を示すイメージです。
  • たとえば、名前を「山田」→「佐藤」→「山田」と変更した場合、最終的な値が初期値と同じなら dirty=false となります。

TanStack Form には 「現在値 ≠ 初期値」を返すフラグがまだ無い ため、defaultValuesuseStore(form.store, s => s.values) が返す現在値を比較するヘルパーを自作する必要があります。

1-1 フォーム定義

事前にフォームを定義します。

import { useForm } from "@tanstack/react-form";

type FormValues = {
  name: string;
  age: number;
  address: { city: string; zip: string };
  tags: string[];
};
export const form = useForm({
  defaultValues: {
    name: "",
    age: 0,
    address: { city: "", zip: "" },
    tags: [],
  } satisfies FormValues,
  onSubmit: async ({ value }) => {
    
    console.log(value);
  },
});

1-2 Dirty パス配列を返すカスタムフック

このカスタムフックは、TanStack Form の useStore を使って現在のフォーム値と初期値を比較し、変更されたフィールドのパスを平坦配列で返します。

この実装は、次章で紹介する RHF のディスカッションで提案されている方法を参考にしています。

import { useStore } from "@tanstack/react-form";
import isEqual from "lodash/isEqual";

type FormOfT> = ReactFormExtendedApiT>;


export const useDirtyKeys = TValues extends Recordstring, unknown>>(
  form: FormOfTValues>
) => {
  
  const values = useStore(form.store, (s) => s.values);
  const defaults = form.options.defaultValues;

  
  const getDirtyFieldPaths = (
    current: any,
    defaultValue: any,
    basePath = ""
  ): string[] => {
    
    if (
      typeof current !== "object" ||
      current === null ||
      defaultValue === null
    ) {
      return !isEqual(current, defaultValue) ? [basePath] : [];
    }

    
    return Object.keys({ ...current, ...defaultValue }).flatMap((key) => {
      const path = basePath ? `${basePath}.${key}` : key;
      const currentValue = current[key];
      const defaultVal = defaultValue?.[key];

      
      if (!isEqual(currentValue, defaultVal)) {
        
        return typeof currentValue === "object" && currentValue !== null
          ? getDirtyFieldPaths(currentValue, defaultVal, path)
          : [path];
      }

      return []; 
    });
  };

  return getDirtyFieldPaths(values, defaults);
};


const dirtyKeys = useDirtyKeys(form);

console.log(dirtyKeys); 

なぜ lodash.isEqual を使うのか?

単純に JSON.stringify で比較する方法も考えられますが、オブジェクトのキー順序の違いや Date オブジェクトの扱いなどで予期せぬ誤差が生じる可能性があります。そのため、オブジェクトの深い比較に特化した lodash.isEqual のようなユーティリティを利用することで、より確実に値の同一性を判定できます。

1-3 派生ストアでパフォーマンス最適化

form.store.derive() を使うことで Derived Store(派生ストア)を作成できます。
Derived Store は、元の Store の値から新しい値(ここでは「差分キー配列」)を計算して保持するストアです。

export const createDirtyStore = TValues extends Recordstring, unknown>>(
  form: FormOfTValues>
) => {
  return form.store.derive((s) =>
    getDirtyFieldPaths(s.values, form.options.defaultValues)
  );
};

export const useDirtyKeysDerived = TValues extends Recordstring, unknown>>(
  form: FormOfTValues>
) => {
  const dirtyStore = useMemo(() => createDirtyStore(form), [form]);
  return useStore(dirtyStore);
};


const dirtyKeys = useDirtyKeysDerived(form);

console.log(dirtyKeys); 

ここで重要なのは、form.store.derive() のコールバック内で deep-diff(getDirtyFieldPaths)の計算を行っている点です。

この派生ストアは state.valuesform.options.defaultValues だけを購読しているため、Dirty 状態の変化以外では再レンダが発生しません

つまり、「差分キー配列だけを 1 つの store に閉じ込めて購読」することで、フォーム全体のパフォーマンスを最適化できます。
大規模なフォームでは、この派生ストアを使うことでパフォーマンスを大幅に向上させることができます。

1-4 差分オブジェクトで PATCH 送信

このカスタムフックは、現在のフォーム値と初期値を比較して変更された部分だけを含むオブジェクトを返します。

こちらも、RHF のコミュニティディスカッションで提案されている実装を参考にしています。

type FormOfT> = ReactFormExtendedApiT>;


export const useDirtyPayload = TValues extends Recordstring, unknown>>(
  form: FormOfTValues>
) => {
  const vals = useStore(form.store, (s) => s.values);
  const defs = form.options.defaultValues;

  const getDirtyValues = T extends Recordstring, any>>(
    current: T,
    defaults: PartialT>
  ): PartialT> => {
    
    if (isEqual(current, defaults)) return {};

    
    if (
      typeof current !== "object" ||
      current === null ||
      Array.isArray(current)
    ) {
      return current;
    }

    
    return Object.keys({ ...current, ...defaults }).reduce((acc, key) => {
      const currentValue = current[key];
      const defaultValue = defaults?.[key];

      
      if (!isEqual(currentValue, defaultValue)) {
        
        const dirtyValue =
          typeof currentValue === "object" &&
          currentValue !== null &&
          !Array.isArray(currentValue)
            ? getDirtyValues(currentValue, defaultValue as any)
            : currentValue;

        
        if (
          typeof dirtyValue === "object" &&
          Object.keys(dirtyValue).length === 0
        ) {
          return acc;
        }

        return { ...acc, [key]: dirtyValue };
      }
      return acc;
    }, {} as PartialT>);
  };

  return getDirtyValues(vals, defs);
};


const payload = useDirtyPayload(form);

console.log(payload); 

fetch("/api/profile", { method: "PATCH", body: JSON.stringify(payload) });
送信後にデフォルト値を更新したい場合

ユーザーが「保存」した直後に その時点の値 を新しい defaultValues
として採用したいケースでは、次のどちらかを呼び出します。

form.reset(newValues); 

form.setDefaultValues(newValues); 

これにより form.options.defaultValues が書き換わります。

RHF には formState.dirtyFields が標準で用意されており、これを活用して変更されたフィールドの処理が可能です。

2-1 変更値を抽出する getDirtyValues 関数

React Hook Form の dirtyFields を効果的に活用するには、変更された値だけを抽出するユーティリティがあると便利です。
React Hook Form のコミュニティディスカッションで提案されている getDirtyValues 関数が最も実用的なアプローチだと思います。

export type DirtyFieldsType =
  | boolean
  | null
  | {
      [key: string]: DirtyFieldsType;
    }
  | DirtyFieldsType[];


export function getDirtyValuesT extends Recordstring, any>>(
  dirtyFields: PartialRecordkeyof T, DirtyFieldsType>>,
  values: T
): PartialT> {
  const dirtyValues = Object.keys(dirtyFields).reduce((prev, key) => {
    const value = dirtyFields[key];
    
    if (!value) {
      return prev;
    }
    const isObject = typeof value === "object";
    const isArray = Array.isArray(value);
    
    const nestedValue =
      isObject && !isArray
        ? getDirtyValues(value as Recordstring, any>, values[key])
        : values[key];
    
    return { ...prev, [key]: isArray ? values[key] : nestedValue };
  }, {} as PartialT>);
  return dirtyValues;
}

この getDirtyValues 関数で実現できる主な機能:

  1. PATCH リクエスト向けデータ生成: 変更されたフィールドの値だけを含むオブジェクトが得られる
  2. 変更フィールドの把握: Object.keys(getDirtyValues(dirtyFields, values)) で変更フィールドのキーが取得できる

配列フィールドの扱いには注意が必要で、この実装では「配列内要素が変更された場合は配列全体を変更対象とする」というシンプルな方針を取っています。

React Hook Form の formState.dirtyFields はリアルタイム差分 です。
現在値が defaultValues と異なるフィールドだけを保持しており、値を初期値に戻すと自動でそのキーが削除されます。
したがって「一度でも編集したら永久に true」という履歴指向ではありません。

2-2 フォーム全体での使用例

import { useForm } from "react-hook-form";
import { getDirtyValues, DirtyFieldsType } from "./form-utils"; 

type FormValues = {
  name: string;
  age: number;
  address: { city: string; zip: string };
  tags: string[];
};

export default function ProfileForm({
  defaultValues,
}: {
  defaultValues: FormValues;
}) {
  const {
    register,
    formState: { dirtyFields, isDirty },
    handleSubmit,
  } = useFormFormValues>({
    defaultValues,
  });

  const onSubmit = (data: FormValues) => {
    
    const dirtyValues = getDirtyValues(dirtyFields, data);
    
    console.log(dirtyValues); 

    
    const diffKeys = Object.keys(dirtyValues);
    console.log(diffKeys); 

    
    fetch("/api/profile", {
      method: "PATCH",
      body: JSON.stringify(dirtyValues),
    });
  };

  return (
    form onSubmit={handleSubmit(onSubmit)}>
      input {...register("name")} placeholder="名前" />
      input {...register("age")} type="number" placeholder="年齢" />
      input {...register("address.city")} placeholder="都市" />
      {}

      button type="submit" disabled={!isDirty}>
        保存
      button>
    form>
  );
}

この方法を使うことで、変更されたフィールドだけを抽出して送信できるため、PATCH リクエスト時のデータ転送量を効率的に削減できます。

2-3 フィールド単位の isDirty 判定

フォーム全体の変更状態だけでなく、個別フィールドの変更状態も簡単に取得できます。
useController フックを使えば、特定フィールドの isDirty 状態を取得できます:

const { control } = useForm({
  defaultValues: { username: "foo" },
});

const {
  fieldState: { isDirty },
} = useController({ name: "username", control });



ポイントは useForm で defaultValues を必ず指定 すること。
デフォルト値が未定義だと比較対象がなくなり isDirty が期待とずれるので注意です。

useController を挟まずに register だけで取得したい場合はformState.dirtyFields[name] を見るか、getFieldState(name) を呼びます。

3-1 RHF における Proxy と購読最適化

React Hook Form は Proxy ベースの遅延評価 を採用し、
参照された FormState のプロパティだけ を購読対象にする」ことで
大規模フォームでも再レンダリングと計算コストを最小化します。


const { dirtyFields } = useFormState({ control });

Proxy による遅延評価

React Hook Form(RHF)は、Proxy ベースの遅延評価を採用しています。
この仕組みの中心となるのが getProxyFormState 関数です。
この関数は FormState オブジェクト全体を Proxy でラップし、アクセスされたプロパティのみを購読対象にします。

この設計により、プロパティにアクセスした時点で対応するフラグが設定されるという点です。
たとえば formState.dirtyFields にアクセスした場合のみ dirty 判定が行われ、不要な計算や再レンダーを避けることができます。
この設計により、実際に利用されるプロパティだけが監視対象となり、フォームのパフォーマンスが最適化されます。

参考:

useFormState による最適化


function ParentForm() {
  const { control } = useForm();
  return (
    form>
      Input name="name" control={control} />
      ErrorDisplay control={control} />
    form>
  );
}


function ErrorDisplay({ control }) {
  
  const { errors } = useFormState({ control });
  if (!errors.name) return null;
  return p>{errors.name.message}p>;
}
  • useFormState を使うと コンポーネント単位で必要な状態だけを購読 できます
  • フォーム全体の状態を親で購読すると、どこか一箇所の変更で全体が再レンダーされる問題を解消
  • name プロパティを指定すれば特定フィールドのみ監視も可能

DirtyFields の内部更新最適化

以下のコードで dirtyFields の更新が最適化されていることがわかります。

https://github.com/fullstackhouse/react-hook-form/blob/5e4429ebaf2d792c10be12ffbdf668a1987aff4b/src/logic/createFormControl.ts#L334-L356

具体的には以下のようなロジックで更新されています。

  • フィールド更新時、購読フラグが立っている場合のみ dirtyFields 全体を再計算
  • 通常は変更のあった特定フィールドだけ set/unset するため効率的
  • dirtyFields オブジェクトはミュータブルに更新され、必要な再レンダー通知だけを行う設計

3-2 TanStack Form のストア購読と派生値

TanStack Form は Store 指向 のため、購読とリアクティビティに異なるアプローチを取ります。


const dirtyCount = form.useStore(
  (s) => Object.values(s.fieldMeta).filter((m) => m.isDirty).length
);

ストア購読の最適化

TanStack Form では、useStore フックを使ってフォーム状態を購読します。公式ドキュメントでも「セレクタを使って必要な状態だけを購読すること」が推奨されています。

https://tanstack.com/form/latest/docs/framework/react/guides/reactivity

  • form.useStore(selector) は内部的に useSyncExternalStoreWithSelector を利用しており、セレクタが返す値を浅い比較で判定します。
  • セレクタで選択した値だけを監視し、前回と異なる場合のみ再レンダリングが発生します。
  • オブジェクトをそのまま返さず、プリミティブや配列・値そのものを返すことが重要です(例:{ a: state.a } ではなく state.a を直接返す)。これにより、不要な再レンダリングを防げます。

また、公式ドキュメントでも「セレクタを省略すると不要な再レンダリングが発生する可能性がある」と警告されています。
最適化のためには、常にセレクタを明示的に指定し、必要な値だけを購読することがベストプラクティスです。

派生ストアによる計算コスト最適化

既に 1-3 で触れた内容ではありますが、パフォーマンスに関して再度整理します。

派生ストアを再掲
export const createDirtyStore = TValues extends Recordstring, unknown>>(
  form: FormOfTValues>
) => {
  return form.store.derive((s) =>
    getDirtyFieldPaths(s.values, form.options.defaultValues)
  );
};

export const useDirtyKeysDerived = TValues extends Recordstring, unknown>>(
  form: FormOfTValues>
) => {
  const dirtyStore = useMemo(() => createDirtyStore(form), [form]);
  return useStore(dirtyStore);
};


const dirtyKeys = useDirtyKeysDerived(form);

console.log(dirtyKeys); 
  • form.store.derive()派生値 を遅延評価で作成可能
  • 依存元(フォーム状態)が変わったときだけ再計算されるため効率的
  • 複雑な深い比較などを派生ストアの中だけで行い、比較結果だけを返すことでパフォーマンス向上
  • コンポーネントは計算結果だけを購読するため、再レンダリングが最小限に抑えられる

参考:

3-3 配列操作時の注意点

RHF の配列操作と dirty 判定

  • useFieldArray で要素を追加・削除すると、デフォルト値との比較で 配列全体 が変化したと判断される
  • 例:10 個の配列から 1 つ削除すると、残りのフィールドもすべて「変更された」扱いになりがち
  • 対策:配列操作後に reset でデフォルト値を更新するか、独自にフラグ管理する

参考:

TanStack Form の配列操作

  • TanStack Form では、各フィールドが独立した isDirty フラグを持つ。
    • そのため、配列の要素を追加・削除・移動しても、他の要素の isDirty 状態には自動的に影響しない。
    • 例えば、あるフィールドを dirty 状態(isDirty=true)にした後、値を元に戻しても isDirtyfalse にはならない。
    • これは「一度 dirty になったら履歴として残る」という設計思想によるもの。
  • reset メソッドを使うことで、すべての isDirty フラグを false にリセットできる。
  • 配列の大幅な変更や初期化が必要な場合は、reset を活用することで isDirty 状態をリセットできる。

このように、TanStack Form では配列操作と isDirty の関係が明確に分離されており、履歴型の dirty 判定が行われます。

参考:

これまで見てきたように、TanStack Form は現在の isDirty フラグが履歴指向であるのに対し、新たに検討が進められている isDefaultValue は現在値と初期値の比較に基づく差分指向です。

以下の PR で対応が進められています

https://github.com/TanStack/form/pull/1456

これが入れば Dirty キー抽出は ワンライナーで可能になります。

const dirtyNow = Object.entries(form.state.fieldMeta)
  .filter(([, m]) => !m.isDefaultValue)
  .map(([k]) => k);

既存 isDirty(履歴指向)と isDefaultValue(差分指向)が並立し、
RHF と同じ「現在値ベースの Dirty 判定」が TanStack でも簡単に書けるようになります。

4-1 isDefaultValue の内部設計と最適化

TanStack Form では従来、一度編集されたフィールドは元の値に戻しても isDirty=true のままでした。
これに対し、「現在値 = デフォルト値なら dirty でない」と判定できる isDefaultValue メタフラグが追加されます。

https://github.com/harry-whorlow/form/blob/cebae4fbaf439c5d5a5ac89b2175b073ab1b50c2/packages/form-core/src/FormApi.ts#L1801-L1816

両フラグの使い分け

フラグ 特性 用途
isDirty 履歴型:一度変更されたら true のまま 「編集履歴があるか」の判定
isDefaultValue 差分型:現在値がデフォルト値と一致すれば true 「現在値が変わっているか」の判定

field.setValue("new-value");
console.log(field.getMeta().isDirty); 
console.log(field.getMeta().isDefaultValue); 

field.setValue("default-value"); 
console.log(field.getMeta().isDirty); 
console.log(field.getMeta().isDefaultValue); 

field.resetField();
console.log(field.getMeta().isDirty); 
console.log(field.getMeta().isDefaultValue); 

4-2 RHF と TanStack Form の比較詳細

実装の複雑さをざっくり比較

観点 React Hook Form (RHF) TanStack Form (+ isDefaultValue)
Dirty 情報の持ち方 dirtyFields というネストツリー。フィールドが深くなるほど構造もどんどん複雑に。 各フィールドに**isDefaultValueの真偽値**を持たせるだけ
更新アルゴリズム 1. パス文字列(user.name / tags[0])を解析
2. set/unsetでツリーを再帰構築
3. 配列のズレ補正
4. 差分判定
1. 値が変わるたび===で「初期値か」だけ再計算
2. メタ情報を O(1)で更新
Dirty キー取得 ユーティリティで再帰 flattenが必要 filtermapで一発

RHF は多機能ですが、ツリー構造の操作が少し複雑になりがちです。一方、TanStack Form はシンプルに管理できるため、コードも計算もすっきりと書けると感じます。

柔軟性・拡張性の違い

何を変えたいか RHF TanStack Form (+ isDefaultValue)
特定フィールドだけ初期値を再設定 reset({ … })で全体置き換えが基本 form.resetField('name', { defaultValue: 'Bob'}) ✔︎
送信後に新しいデフォルト値を注入 reset(values)で全フィールド一括 form.setDefaultValues(values)が公式 API ✔︎
Dirty 判定ロジックを上書き 内部実装依存でカスタムしづらい isDefaultValueを直接操作・独自メタ追加も OK ✔︎
dirtyFieldsisDirtyの整合 場合によっては手動リセットが必要 isDirty(履歴)とisDefaultValue(差分)が明確に分離 ✔︎

RHF は基本的に公式が用意した枠組みの中で工夫するイメージですが、TanStack Form はメタ情報の設計を後から自由に拡張できるため、より柔軟にさまざまな要件に対応しやすいと感じます。


4-4 isDefaultValue 導入後の簡略化実装例

isDefaultValue が導入されると、1-2, 1-4 で示した複雑な実装は大幅に簡略化されます。以下に具体例を示します。

現在の useDirtyKeys が以下のようになります


export const useDirtyKeys = TValues extends Recordstring, unknown>>(
  form: FormOfTValues>
) => {
  
  const fieldMeta = useStore(form.store, (s) => s.fieldMeta);

  
  return Object.entries(fieldMeta)
    .filter(([, meta]) => !meta.isDefaultValue)
    .map(([key]) => key);
};

現在の useDirtyPayload も簡略化できる想定です。


export const useDirtyPayload = TValues extends Recordstring, unknown>>(
  form: FormOfTValues>
) => {
  const values = useStore(form.store, (s) => s.values);
  const fieldMeta = useStore(form.store, (s) => s.fieldMeta);

  
  return Object.entries(fieldMeta)
    .filter(([, meta]) => !meta.isDefaultValue)
    .reduce((result, [key]) => {
      const parts = key.split(".");

      
      const setNestedValue = (obj, valueObj, pathParts) => {
        if (pathParts.length === 1) {
          return {
            ...obj,
            [pathParts[0]]: valueObj[pathParts[0]],
          };
        }
        const [head, ...rest] = pathParts;
        return {
          ...obj,
          [head]: setNestedValue(obj[head] || {}, valueObj[head], rest),
        };
      };

      return setNestedValue(result, values, parts);
    }, {});
};

このように、これまで必要だった複雑な再帰処理や深い比較を行う実装が、シンプルなフィルタリングと値の抽出だけで済むようになります。

isDefaultValue の導入によって、TanStack Form の実装はより簡潔になり、コード量も大きく減らせる見込みです。

本記事では、TanStack Form と React Hook Form における「Dirty 判定」の挙動の違いについて解説しました。

両ライブラリは根本的な検知方式が異なります。React Hook Form は変更されたフィールドをネスト構造で保持し追跡するのに対し、TanStack Form は現状では自前で実装した比較ロジックで初期値との差分を検出する必要があります。

また、TanStack Form は isDefaultValue の導入が進行中で、将来的には履歴型と差分型の両方のアプローチを柔軟に使い分けられるようになる予定です。

両ライブラリにはそれぞれメリットとデメリットがあり、どちらが絶対的に優れているというより、特定のユースケースに最適なライブラリを選ぶことが大切に感じます。フォームの複雑さや要件に応じて、この記事で紹介した知見を参考にしていただければ幸いです。

以上です!

フラッグシティパートナーズ海外不動産投資セミナー 【DMM FX】入金

Source link

Views: 0

RELATED ARTICLES

返事を書く

あなたのコメントを入力してください。
ここにあなたの名前を入力してください

- Advertisment -

Most Popular

Recent Comments