# Controller

`Controller` is a singleton providing safe access to the Reactive Data Client [flux store and lifecycle](https://dataclient.io/docs/api/Manager.md#control-flow).
`Controller` memoizes all store access, allowing a global referential equality guarantee and the fastest rendering
and retrieval performance.

`Controller` is provided:

- [Managers](https://dataclient.io/docs/api/Manager.md) as the first argument in [Manager.middleware](https://dataclient.io/docs/api/Manager.md#middleware)
- React with [useController()](https://dataclient.io/docs/api/useController.md)
- [Unit testing hooks](https://dataclient.io/docs/guides/unit-testing-hooks.md) with [renderDataHook()](https://dataclient.io/docs/api/renderDataHook.md#controller)

```ts
class Controller {
  /*************** Action Dispatchers ***************/
  fetch(endpoint, ...args): ReturnType<E>;
  fetchIfStale(endpoint, ...args): ReturnType<E> | undefined;
  expireAll({ testKey }): Promise<void>;
  invalidate(endpoint, ...args): Promise<void>;
  invalidateAll({ testKey }): Promise<void>;
  resetEntireStore(): Promise<void>;
  set(queryable, ...args, value): Promise<void>;
  set([Entity], rows): Promise<void>;
  setResponse(endpoint, ...args, response): Promise<void>;
  setError(endpoint, ...args, error): Promise<void>;
  resolve(endpoint, { args, response, fetchedAt, error }): Promise<void>;
  subscribe(endpoint, ...args): Promise<void>;
  unsubscribe(endpoint, ...args): Promise<void>;
  /*************** Data Access ***************/
  get(queryable, ...args, state): Denormalized<typeof queryable>;
  getResponse(endpoint, ...args, state): { data; expiryStatus; expiresAt };
  getError(endpoint, ...args, state): ErrorTypes | undefined;
  snapshot(state: State<unknown>, fetchedAt?: number): SnapshotInterface;
  getState(): State<unknown>;
}
```

## Action Dispatchers

### fetch(endpoint, ...args) {#fetch}

Fetches the endpoint with given args, updating the Reactive Data Client cache with
the response or error upon completion.

**Create**

```tsx
import { useController } from '@data-client/react';
import { PostResource } from './PostResource';

function CreatePost() {
  const ctrl = useController();

  return (
    <form
      onSubmit={e =>
        ctrl.fetch(PostResource.getList.push, new FormData(e.currentTarget))
      }
    >
      {/* ... */}
    </form>
  );
}
```

**Update**

```tsx
import { useController } from '@data-client/react';
import { PostResource } from './PostResource';

function UpdatePost({ id }: { id: string }) {
  const ctrl = useController();

  return (
    <form
      onSubmit={e =>
        ctrl.fetch(PostResource.update, { id }, new FormData(e.currentTarget))
      }
    >
      {/* ... */}
    </form>
  );
}
```

**Delete**

```tsx
import { useController } from '@data-client/react';
import { useCallback } from 'react';
import { useNavigate } from 'react-router';
import { Post, PostResource } from './PostResource';

function PostListItem({ post }: { post: Post }) {
  const ctrl = useController();
  const navigate = useNavigate();

  const handleDelete = useCallback(
    async e => {
      await ctrl.fetch(PostResource.delete, { id: post.id });
      navigate('/');
    },
    [ctrl, post.id],
  );

  return (
    <div>
      <h3>{post.title}</h3>
      <button onClick={handleDelete}>X</button>
    </div>
  );
}
```

> **Tip**
>
> `fetch` has the same return value as the [Endpoint](https://dataclient.io/rest/api/Endpoint.md) passed to it.
> When using schemas, the denormalized value is returned
>
> ```ts
> const controller = useController();
>
> const post = await controller.fetch(
>   PostResource.getList.push,
>   createPayload,
> );
> post.title;
> post.pk();
> ```

#### Endpoint.sideEffect

[sideEffect](https://dataclient.io/rest/api/Endpoint.md#sideeffect) changes the behavior

##### true

- Resolves _before_ [committing](https://react.dev/learn/render-and-commit#step-3-react-commits-changes-to-the-dom) Reactive Data Client cache updates. (React 16, 17)
- Each call will always cause a new fetch.

##### false | undefined

- Resolves _after_ [committing](https://react.dev/learn/render-and-commit#step-3-react-commits-changes-to-the-dom) Reactive Data Client cache updates.
- Identical requests are deduplicated globally; allowing only one inflight request at a time.
  - To ensure a _new_ request is started, make sure to abort any existing inflight requests.

### fetchIfStale(endpoint, ...args) {#fetchIfStale}

Fetches only if endpoint is considered '[stale](https://dataclient.io/docs/concepts/expiry-policy.md#stale)'.

This can be useful when prefetching data, as it avoids overfetching fresh data.

An [example](https://stackblitz.com/github/reactive/data-client/tree/master/examples/github-app?file=src%2Frouting%2Froutes.tsx) with a fetch-as-you-render router:

```ts
{
  name: 'IssueList',
  component: lazyPage('IssuesPage'),
  title: 'issue list',
  resolveData: async (
    controller: Controller,
    { owner, repo }: { owner: string; repo: string },
    searchParams: URLSearchParams,
  ) => {
    const q = searchParams?.get('q') || 'is:issue is:open';
    await controller.fetchIfStale(IssueResource.search, {
      owner,
      repo,
      q,
    });
  },
},
```

Example app: [github-app](https://github.com/reactive/data-client/tree/master/examples/github-app) ([`src/routing/routes.tsx`](https://github.com/reactive/data-client/blob/master/examples/github-app/src/routing/routes.tsx))

### expireAll({ testKey }) {#expireAll}

Sets all responses' [expiry status](https://dataclient.io/docs/concepts/expiry-policy.md) matching `testKey` to [Stale](https://dataclient.io/docs/concepts/expiry-policy.md#stale).

This is sometimes useful to trigger refresh of only data presently shown
when there are many parameterizations in cache.

```tsx
import { type Controller, useController } from '@data-client/react';
import { AccountResource, TradeResource, type Trade } from './resources';
import { Form, FormField } from './Form';

const createTradeHandler =
  (ctrl: Controller, userId: string) => async (trade: Trade) => {
    await ctrl.fetch(TradeResource.getList.push, { user: userId }, trade);
    ctrl.expireAll(AccountResource.get);
    ctrl.expireAll(AccountResource.getList);
  };

function CreateTrade({ userId }: { userId: string }) {
  const handleTrade = createTradeHandler(useController(), userId);

  return (
    <Form onSubmit={handleTrade}>
      <FormField name="ticker" />
      <FormField name="amount" type="number" />
      <FormField name="price" type="number" />
    </Form>
  );
}
```

> **Tip**
>
> To reduce load, improve performance, and improve state consistency; it can often be
> better to [include mutation sideeffects in the mutation response](https://dataclient.io/rest/guides/side-effects.md).

### invalidate(endpoint, ...args) {#invalidate}

Forces refetching and suspenseon [useSuspense](https://dataclient.io/docs/api/useSuspense.md) with the same Endpoint
and parameters.

```tsx
import { useController, useSuspense } from '@data-client/react';
import { ArticleResource } from './ArticleResource';

function ArticleName({ id }: { id: string }) {
  const article = useSuspense(ArticleResource.get, { id });
  const ctrl = useController();

  return (
    <div>
      <h1>{article.title}</h1>
      <button onClick={() => ctrl.invalidate(ArticleResource.get, { id })}>
        Fetch &amp; suspend
      </button>
    </div>
  );
}
```

> **Tip**
>
> To refresh while continuing to display stale data - [Controller.fetch](#fetch).

> **Tip: Invalidate many endpoints at once**
>
> Use [schema.Invalidate](https://dataclient.io/rest/api/Invalidate.md) to invalidate every endpoint that contains a given entity.
>
> For REST try using [Resource.delete](https://dataclient.io/rest/api/resource.md#delete)
>
> ```ts
> // deletes MyResource(5)
> // this will refetch MyResource.get({id: '5'})
> // and remove it from MyResource.getList
> controller.setResponse(MyResource.delete, { id: '5' }, { id: '5' });
> ```

### invalidateAll({ testKey }) {#invalidateAll}

[Invalidates](https://dataclient.io/docs/concepts/expiry-policy.md#invalid) all [endpoint keys](https://dataclient.io/rest/api/RestEndpoint.md#key) matching `testKey`.

```tsx
import { useController, useSuspense } from '@data-client/react';
import { ArticleResource } from './ArticleResource';

function ArticleName({ id }: { id: string }) {
  const article = useSuspense(ArticleResource.get, { id });
  const ctrl = useController();

  return (
    <div>
      <h1>{article.title}</h1>
      <button onClick={() => ctrl.invalidateAll(ArticleResource.get)}>
        Fetch &amp; suspend
      </button>
    </div>
  );
}
```

> **Tip**
>
> To refresh while continuing to display stale data - [Controller.expireAll](#expireAll) instead.

Here we clear only GET endpoints using the test.com domain. This means other domains remain in cache.

```ts
const myDomain = 'http://test.com';
const testKey = (key: string) => key.startsWith(`GET ${myDomain}`);

function useLogout() {
  const ctrl = useController();
  return () => ctrl.invalidateAll({ testKey });
}
```

It's usually a good idea to also clear cache on 401 (unauthorized) with [LogoutManager](https://dataclient.io/docs/api/LogoutManager.md)
as well.

**Web**

```tsx title="index.tsx"
import {
  DataProvider,
  LogoutManager,
  getDefaultManagers,
} from '@data-client/react';
import { createRoot } from 'react-dom/client';
import App from './App';
import { unAuth } from '../authentication';

const myDomain = 'http://test.com';
const testKey = (key: string) => key.startsWith(`GET ${myDomain}`);

const managers = [
  new LogoutManager({
    handleLogout(controller) {
      // call custom unAuth function we defined
      unAuth();
      // still reset the store
      controller.invalidateAll({ testKey });
    },
  }),
  ...getDefaultManagers(),
];

createRoot(document.body).render(
  <DataProvider managers={managers}>
    <App />
  </DataProvider>,
);
```

**React Native**

```tsx title="index.tsx"
import {
  DataProvider,
  LogoutManager,
  getDefaultManagers,
} from '@data-client/react';
import { AppRegistry } from 'react-native';
import App from './App';
import { unAuth } from '../authentication';

const myDomain = 'http://test.com';
const testKey = (key: string) => key.startsWith(`GET ${myDomain}`);

const managers = [
  new LogoutManager({
    handleLogout(controller) {
      // call custom unAuth function we defined
      unAuth();
      // still reset the store
      controller.invalidateAll({ testKey });
    },
  }),
  ...getDefaultManagers(),
];

const Root = () => (
  <DataProvider managers={managers}>
    <App />
  </DataProvider>
);
AppRegistry.registerComponent('MyApp', () => Root);
```

**NextJS**

```tsx title="app/Provider.tsx"
'use client';
import { LogoutManager, getDefaultManagers } from '@data-client/react';
import { DataProvider } from '@data-client/react/nextjs';
import { unAuth } from '../authentication';

const myDomain = 'http://test.com';
const testKey = (key: string) => key.startsWith(`GET ${myDomain}`);

const managers = [
  new LogoutManager({
    handleLogout(controller) {
      // call custom unAuth function we defined
      unAuth();
      // still reset the store
      controller.invalidateAll({ testKey });
    },
  }),
  ...getDefaultManagers(),
];

export default function Provider({
  children,
}: {
  children: React.ReactNode;
}) {
  return <DataProvider managers={managers}>{children}</DataProvider>;
}
```

```tsx title="app/layout.tsx"
import Provider from './Provider';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Provider>{children}</Provider>
      </body>
    </html>
  );
}
```

**Expo**

```tsx title="app/_layout.tsx"
import { Stack } from 'expo-router';
import {
  DataProvider,
  LogoutManager,
  getDefaultManagers,
} from '@data-client/react';
import { unAuth } from '../authentication';

const myDomain = 'http://test.com';
const testKey = (key: string) => key.startsWith(`GET ${myDomain}`);

const managers = [
  new LogoutManager({
    handleLogout(controller) {
      // call custom unAuth function we defined
      unAuth();
      // still reset the store
      controller.invalidateAll({ testKey });
    },
  }),
  ...getDefaultManagers(),
];

export default function RootLayout() {
  return (
    <DataProvider managers={managers}>
      <Stack>
        <Stack.Screen name="index" />
      </Stack>
    </DataProvider>
  );
}
```

### resetEntireStore() {#resetEntireStore}

Resets/clears the entire Reactive Data Client cache. All inflight requests will not resolve.

This is typically used when logging out or changing authenticated users.

```tsx
import { useController, useSuspense } from '@data-client/react';
import { useCallback } from 'react';
import { CurrentUserResource } from './CurrentUserResource';
import { impersonateUser } from './auth';

const USER_NUMBER_ONE: string = '1111';

function UserName() {
  const user = useSuspense(CurrentUserResource.get);
  const ctrl = useController();

  const becomeAdmin = useCallback(() => {
    // Changes the current user
    impersonateUser(USER_NUMBER_ONE);
    ctrl.resetEntireStore();
  }, [ctrl]);
  return (
    <div>
      <h1>{user.name}</h1>
      <button onClick={becomeAdmin}>Be Number One</button>
    </div>
  );
}
```

### set(queryable, ...args, value) {#set}

Updates any [Queryable](https://dataclient.io/rest/api/schema.md#queryable) [Schema](https://dataclient.io/rest/api/schema.md#schema-overview), or many entities at once with an [Array](https://dataclient.io/rest/api/Array.md) or [Values](https://dataclient.io/rest/api/Values.md) schema.

```ts
ctrl.set(
  Todo,
  // which Todo to update
  { id: '5' },
  // merge this data into the Todo in the store
  { id: '5', title: 'tell me friends how great Data Client is' },
);
```

The value is typed by the schema: an [Entity](https://dataclient.io/rest/api/Entity.md) takes its fields (numbers and strings may be either),
while a [Collection](https://dataclient.io/rest/api/Collection.md) or [All](https://dataclient.io/rest/api/All.md) takes a list of rows. A [Query](https://dataclient.io/rest/api/Query.md)
takes the input of the schema it wraps, since `set()` normalizes that schema rather than reversing `process()`.

```ts
ctrl.set(TodoResource.getList.schema, [{ id: '5', completed: true }]);
```

> **Note: Unions**
>
> When each member declares its discriminator as a literal (like `readonly type = 'first'`), a
> [Union](https://dataclient.io/rest/api/Union.md) row is checked against the member it selects, so `{ type: 'first', secondField: 1 }` is an
> error. Only declared fields are accepted, so a key read by a
> `schemaAttribute` function must be declared on each member.

Functions can be used in the value when derived data is used. This [prevents race conditions](https://react.dev/reference/react/useState#updating-state-based-on-the-previous-state).

```ts
const id = '2';
ctrl.set(Article, { id }, article => ({ id, votes: article.votes + 1 }));
```

#### set(\[Entity], rows) {#set-array}

Pass an [Array](https://dataclient.io/rest/api/Array.md) schema (`[Todo]` or `new schema.Array(Todo)`) and a list of rows to update
many entities in one store update. Each row merges with its stored entity; entities not in the list are untouched.

```ts
ctrl.set(
  [Todo],
  [
    { id: '5', completed: true },
    { id: '6', completed: false },
  ],
);
```

Rows are typed by the Entity's fields; numbers and strings may be either, and object, array and Date values are not
checked since rows are raw input.

For lists that mix Entity types, use a [Union](https://dataclient.io/rest/api/Union.md); each row is stored by its `type`:

```ts
const Feed = new schema.Union({ post: Post, comment: Comment }, 'type');

ctrl.set(
  [Feed],
  [
    { id: '1', type: 'post', title: 'Hello' },
    { id: '7', type: 'comment', body: 'Nice!' },
  ],
);
```

To delete many entities at once, use [Invalidate](https://dataclient.io/rest/api/Invalidate.md#batch-invalidation); rows only need their pk
fields:

```ts
ctrl.set([new schema.Invalidate(Todo)], [{ id: '5' }, { id: '6' }]);
```

To delete one, pass the Invalidate schema and its row:

```ts
ctrl.set(new schema.Invalidate(Todo), { id: '5' });
```

[Values](https://dataclient.io/rest/api/Values.md) schemas take an object of rows instead:

```ts
ctrl.set(new schema.Values(Todo), {
  '5': { id: '5', completed: true },
  '6': { id: '6', completed: false },
});
```

Array, Values and Invalidate schemas take no `args` (so [Entity.pk()](https://dataclient.io/rest/api/Entity.md#pk) and [Entity.process()](https://dataclient.io/rest/api/Entity.md#process)
receive `[]`) and no updater function. Rows that share a pk merge in list order, without
[Entity.shouldReorder()](https://dataclient.io/rest/api/Entity.md#shouldreorder). Use this instead of calling `set()` once per row, such as when
[batching high-frequency stream updates](https://dataclient.io/docs/concepts/managers.md#batching).

Try both buttons below. This browser check starts from an empty store and times `Promise.all` of 500 `set()`
calls against one batch `set()`. Both paths are one React commit, and each writes 500 new prices.

```ts title="Ticker"
import { Entity } from '@data-client/rest';

export class Ticker extends Entity {
  product_id = '';
  price = 0;

  pk() {
    return this.product_id;
  }
  static key = 'Ticker';
}

export const newPrices = () =>
  Array.from({ length: 500 }, (_, i) => ({
    product_id: `COIN-${i}`,
    price: Math.round(Math.random() * 10000) / 100,
  }));
```

```tsx title="PriceStream"
import React from 'react';
import { useController, useQuery } from '@data-client/react';
import { Ticker, newPrices } from './Ticker';

function PriceStream() {
  const ctrl = useController();
  const [timing, setTiming] = React.useState('');
  const first = useQuery(Ticker, { product_id: 'COIN-0' });

  const time = async (
    label: string,
    write: (rows: ReturnType<typeof newPrices>) => Promise<unknown>,
  ) => {
    const rows = newPrices();
    const start = performance.now();
    await write(rows);
    setTiming(`${label}: ${(performance.now() - start).toFixed(1)} ms`);
  };
  const perRow = () =>
    time('500 set() calls', rows =>
      Promise.all(
        rows.map(row => ctrl.set(Ticker, { product_id: row.product_id }, row)),
      ),
    );
  const batch = () => time('1 batch set()', rows => ctrl.set([Ticker], rows));

  return (
    <div>
      <button onClick={perRow}>set() per row</button>{' '}
      <button onClick={batch}>batch set()</button>
      <p>COIN-0: {first ? `$${first.price}` : 'no data yet'}</p>
      <p>{timing}</p>
    </div>
  );
}
render(<PriceStream />);
```

### setResponse(endpoint, ...args, response) {#setResponse}

Stores `response` in cache for given [Endpoint](https://dataclient.io/rest/api/Endpoint.md) and args.

Any components suspending for the given [Endpoint](https://dataclient.io/rest/api/Endpoint.md) and args will resolve.

If data already exists for the given [Endpoint](https://dataclient.io/rest/api/Endpoint.md) and args, it will be updated.

```tsx
import { useController } from '@data-client/react';
import { useEffect } from 'react';
import { EndpointLookup } from './EndpointLookup';

function useWebsocketUpdates(url: string) {
  const ctrl = useController();

  useEffect(() => {
    const websocket = new WebSocket(url);

    websocket.onmessage = event => {
      const { endpoint, args, data } = JSON.parse(event.data);
      ctrl.setResponse(EndpointLookup[endpoint], ...args, data);
    };

    return () => websocket.close();
  }, [ctrl, url]);
}
```

This shows a proof of concept in React; however a [Manager websockets implementation](https://dataclient.io/docs/concepts/managers.md#data-stream)
would be much more robust.

### setError(endpoint, ...args, error) {#setError}

Stores the result of [Endpoint](https://dataclient.io/rest/api/Endpoint.md) and args as the error provided.

### resolve(endpoint, { args, response, fetchedAt, error }) {#resolve}

Resolves a specific fetch, storing the `response` in cache.

This is similar to setResponse, except it triggers resolution of an inflight fetch.
This means the corresponding optimistic update will no longer be applies.

This is used in [NetworkManager](https://dataclient.io/docs/api/NetworkManager.md), and should be used when
processing fetch requests.

### subscribe(endpoint, ...args) {#subscribe}

Marks a new subscription to a given [Endpoint](https://dataclient.io/rest/api/Endpoint.md). This should increment the subscription.

[useSubscription](https://dataclient.io/docs/api/useSubscription.md) and [useLive](https://dataclient.io/docs/api/useLive.md) call this on mount.

This might be useful for custom hooks to sub/unsub based on other factors.

```tsx
import {
  useController,
  type EndpointInterface,
  type FetchFunction,
  type Schema,
} from '@data-client/react';
import { useEffect } from 'react';

function useSubscribe<
  E extends EndpointInterface<FetchFunction, Schema | undefined, false | undefined>,
>(endpoint: E, ...args: readonly [...Parameters<E>]) {
  const controller = useController();
  const key = endpoint.key(...args);

  useEffect(() => {
    controller.subscribe(endpoint, ...args);
    return () => {
      controller.unsubscribe(endpoint, ...args);
    };
  }, [controller, key]);
}
```

### unsubscribe(endpoint, ...args) {#unsubscribe}

Marks completion of subscription to a given [Endpoint](https://dataclient.io/rest/api/Endpoint.md). This should
decrement the subscription and if the count reaches 0, more updates won't be received automatically.

[useSubscription](https://dataclient.io/docs/api/useSubscription.md) and [useLive](https://dataclient.io/docs/api/useLive.md) call this on unmount.

## Data Access

### get(schema, ...args, state) {#get}

Looks up any [Queryable](https://dataclient.io/rest/api/schema.md#queryable) [Schema](https://dataclient.io/rest/api/schema.md#schema-overview) in `state`.

#### Example

This is used in [useQuery](https://dataclient.io/docs/api/useQuery.md) and can be used in
[Managers](https://dataclient.io/docs/api/Manager.md) to safely access the store.

```tsx title="useQuery.ts"
import {
  useController,
  StateContext,
  type Queryable,
  type SchemaArgs,
  type DenormalizeNullable,
} from '@data-client/react';
import { useContext } from 'react';

/** Oversimplified useQuery */
function useQuery<S extends Queryable>(
  schema: S,
  ...args: SchemaArgs<S>
): DenormalizeNullable<S> | undefined {
  const state = useContext(StateContext);
  const controller = useController();

  return controller.get(schema, ...args, state);
}
```

### getResponse(endpoint, ...args, state) {#getResponse}

```ts title="returns"
{
  data: DenormalizeNullable<E['schema']>;
  expiryStatus: ExpiryStatus;
  expiresAt: number;
}
```

Gets the (globally referentially stable) response for a given endpoint/args pair from state given.

#### data

The denormalize response data. Guarantees global referential stability for all members.

#### [expiryStatus](https://dataclient.io/docs/concepts/expiry-policy.md#expiry-status)

```ts
export enum ExpiryStatus {
  Invalid = 1,
  InvalidIfStale,
  Valid,
}
```

##### Valid

- Will never suspend.
- Might fetch if data is stale

##### InvalidIfStale

- Will suspend if data is stale.
- Might fetch if data is stale

##### Invalid

- Will always suspend
- Will always fetch

#### expiresAt

A number representing time when it expires. Compare to Date.now().

#### Example

This is used in [useCache](https://dataclient.io/docs/api/useCache.md), [useSuspense](https://dataclient.io/docs/api/useSuspense.md) and can be used in
[Managers](https://dataclient.io/docs/api/Manager.md) to lookup a response with the state provided.

```tsx title="useCache.ts"
import {
  useController,
  StateContext,
  type EndpointInterface,
} from '@data-client/react';
import { useContext } from 'react';

/** Oversimplified useCache */
function useCache<E extends EndpointInterface>(
  endpoint: E,
  ...args: readonly [...Parameters<E>]
) {
  const state = useContext(StateContext);
  const controller = useController();
  return controller.getResponse(endpoint, ...args, state).data;
}
```

```tsx title="MyManager.ts"
import {
  type Manager,
  type Middleware,
  actionTypes,
} from '@data-client/react';

export default class MyManager implements Manager {
  declare protected websocket: WebSocket;

  middleware: Middleware = controller => {
    return next => async action => {
      if (action.type === actionTypes.FETCH) {
        console.log('The existing response of the requested fetch');
        console.log(
          controller.getResponse(
            action.endpoint,
            ...action.args,
            controller.getState(),
          ).data,
        );
      }
      next(action);
    };
  };

  cleanup() {
    this.websocket.close();
  }
}
```

### getError(endpoint, ...args, state) {#getError}

Gets the error, if any, for a given endpoint. Returns undefined for no errors.

### snapshot(state, fetchedAt) {#snapshot}

Returns a [Snapshot](https://dataclient.io/docs/api/Snapshot.md).

### getState() {#getState}

Gets the internal state of Reactive Data Client that has _already been [committed](https://react.dev/learn/render-and-commit#step-3-react-commits-changes-to-the-dom)_.

> **Warning**
>
> This should only be used in event handlers or [Managers](https://dataclient.io/docs/api/Manager.md).
>
> Using getState() in React's render lifecycle can result in data tearing.

```tsx
import { useController } from '@data-client/react';
import { useCallback } from 'react';
import { MyResource } from './resources/MyResource';
import { redirect } from './routing';

function useUpdateHandler(id: string) {
  const controller = useController();

  return useCallback(
    async updatePayload => {
      const response = await controller.fetch(
        MyResource.update,
        { id },
        updatePayload,
      );
      // the fetch has completed, but react has not yet re-rendered
      // this lets use sequence after the next re-render
      // we're working on a better solution to this specific case
      setTimeout(() => {
        const { data: denormalized } = controller.getResponse(
          MyResource.update,
          { id },
          updatePayload,
          controller.getState(),
        );
        redirect(denormalized.getterUrl);
      }, 40);
    },
    [id],
  );
}
```
