Repository: tannerlinsley/react-query
File: packages/query-core/src/query.ts (TypeScript)

Code before:
    this.scheduleGc()
  }
  get meta(): QueryMeta | undefined {
    return this.options.meta
  }

  get promise(): Promise<TData> | undefined {
    return this.#retryer?.promise
  }

  setOptions(
    options?: QueryOptions<TQueryFnData, TError, TData, TQueryKey>,
  ): void {
    this.options = { ...this.#defaultOptions, ...options }

    this.updateGcTime(this.options.gcTime)

    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
    if (this.state && this.state.data === undefined) {
      const defaultState = getDefaultState(this.options)
      if (defaultState.data !== undefined) {
        this.setState({
          data: defaultState.data,
          dataUpdatedAt: defaultState.dataUpdatedAt,
          status: 'success',
          error: null,
        })
        this.#initialState = defaultState
      }
    }
  }

  protected optionalRemove() {
    if (!this.observers.length && this.state.fetchStatus === 'idle') {
      this.#cache.remove(this)
    }
  }

  setData(
    newData: TData,
    options?: SetDataOptions & { manual: boolean },
  ): TData {
    const data = replaceData(this.state.data, newData, this.options)

    // Set data and mark it as cached
    this.#dispatch({
      data,
      type: 'success',
      dataUpdatedAt: options?.updatedAt,
      manual: options?.manual,
    })

Review comment (bug):
this is the right direction, but I don’t think the fix is complete.

I think we need to spread `...defaultState` because it does a bunch of other things, like setting `errorUpdateCount: 0`.

I was just checking if that would overwrite too much. Essentially, we want everything that we set unconditionally in `setData`:

https://github.com/TanStack/query/blob/1c8a92167769f322d0206325b98493769afb0961/packages/query-core/src/query.ts#L638-L643

_except_ increasing `dataUpdateCount`.

that means we want:

- ✅ `error` set to `null`, which `getDefaultState` does
- ✅ `isInvalidated` set to `false`, which `getDefaultState` does
- ✅ `data` set to our `data`, which `getDefaultState` does
- ✅ `dataUpdatedAt` set, which `getDefaultState` does
- ✅ `status: 'success'`, also handled by `getDefaultState`

But additionally, we would lose:
- `fetchMeta` gets reset to `null`
- `fetchFailureReason` and `fetchFailureCount` get reset, too. It could be that this is just missing from the `success` action too but not sure.

So I think the best fix would be to extract the code above into a function that we can call in the reducer and here that does what we want:

```
function successState(data, dataUpdatedAt) {
  return {
    data,
    dataUpdatedAt: ataUpdatedAt ?? Date.now(),
    error: null,
    isInvalidated: false,
    status: 'success' as const,
  }
}
```

This would make sure these two places don’t diverge in the future.

Code after:
    this.scheduleGc()
  }
  get meta(): QueryMeta | undefined {
    return this.options.meta
  }

  get promise(): Promise<TData> | undefined {
    return this.#retryer?.promise
  }

  setOptions(
    options?: QueryOptions<TQueryFnData, TError, TData, TQueryKey>,
  ): void {
    this.options = { ...this.#defaultOptions, ...options }

    this.updateGcTime(this.options.gcTime)

    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
    if (this.state && this.state.data === undefined) {
      const defaultState = getDefaultState(this.options)
      if (defaultState.data !== undefined) {
        this.setState(
          successState(defaultState.data, defaultState.dataUpdatedAt),
        )
        this.#initialState = defaultState
      }
    }
  }

  protected optionalRemove() {
    if (!this.observers.length && this.state.fetchStatus === 'idle') {
      this.#cache.remove(this)
    }
  }

  setData(
    newData: TData,
    options?: SetDataOptions & { manual: boolean },
  ): TData {
    const data = replaceData(this.state.data, newData, this.options)

    // Set data and mark it as cached
    this.#dispatch({
      data,
      type: 'success',
      dataUpdatedAt: options?.updatedAt,
      manual: options?.manual,
    })

    return data
  }