Skip to content

Commit

Permalink
feat (ai/react): add isLoading to useObject (#2090)
Browse files Browse the repository at this point in the history
  • Loading branch information
lgrammel committed Jun 25, 2024
1 parent 82d9c8d commit 321a7d0
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 7 deletions.
5 changes: 5 additions & 0 deletions .changeset/lucky-mirrors-admire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@ai-sdk/react': patch
---

feat (ai/react): add isLoading to useObject
12 changes: 8 additions & 4 deletions content/docs/07-reference/ai-sdk-ui/03-use-object.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,14 @@ export default function Page() {
name: 'error',
type: 'undefined | unknown',
description: 'The error object if the API call fails.',

}

]}
},
{
name: 'isLoading',
type: 'boolean',
description:
'Boolean flag indicating whether a request is currently in progress.',
},
]}
/>

## Examples
Expand Down
13 changes: 12 additions & 1 deletion packages/react/src/use-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ export type Experimental_UseObjectHelpers<RESULT, INPUT> = {
* The error object of the API request if any.
*/
error: undefined | unknown;

/**
* Flag that indicates whether an API request is in progress.
*/
isLoading: boolean;
};

function useObject<RESULT, INPUT = any>({
Expand All @@ -69,10 +74,13 @@ function useObject<RESULT, INPUT = any>({
);

const [error, setError] = useState<undefined | unknown>(undefined);
const [isLoading, setIsLoading] = useState(false);

return {
async setInput(input) {
try {
setIsLoading(true);

const response = await fetch(api, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
Expand All @@ -92,7 +100,7 @@ function useObject<RESULT, INPUT = any>({
let accumulatedText = '';
let latestObject: DeepPartial<RESULT> | undefined = undefined;

response.body!.pipeThrough(new TextDecoderStream()).pipeTo(
response.body.pipeThrough(new TextDecoderStream()).pipeTo(
new WritableStream<string>({
write(chunk) {
accumulatedText += chunk;
Expand All @@ -113,10 +121,13 @@ function useObject<RESULT, INPUT = any>({
setError(undefined);
} catch (error) {
setError(error);
} finally {
setIsLoading(false);
}
},
object: data,
error,
isLoading,
};
}

Expand Down
56 changes: 54 additions & 2 deletions packages/react/src/use-object.ui.test.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import { mockFetchDataStream, mockFetchError } from '@ai-sdk/ui-utils/test';
import '@testing-library/jest-dom/vitest';
import { cleanup, render, screen } from '@testing-library/react';
import { cleanup, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { HttpResponse, http } from 'msw';
import { SetupServer, setupServer } from 'msw/node';
import { z } from 'zod';
import { experimental_useObject } from './use-object';

describe('text stream', () => {
const TestComponent = () => {
const { object, error, setInput } = experimental_useObject({
const { object, error, setInput, isLoading } = experimental_useObject({
api: '/api/use-object',
schema: z.object({ content: z.string() }),
});

return (
<div>
<div data-testid="loading">{isLoading.toString()}</div>
<div data-testid="object">{JSON.stringify(object)}</div>
<div data-testid="error">{error?.toString()}</div>
<button
Expand Down Expand Up @@ -64,6 +67,55 @@ describe('text stream', () => {
});
});

describe('isLoading', async () => {
let streamController: ReadableStreamDefaultController<string>;
let server: SetupServer;

beforeEach(() => {
const stream = new ReadableStream({
start(controller) {
streamController = controller;
},
});

server = setupServer(
http.post('https://example.com/api/use-object', ({ request }) => {
return new HttpResponse(stream.pipeThrough(new TextEncoderStream()), {
status: 200,
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
});
}),
);

server.listen();
});

afterEach(() => {
server.close();
});

it('should be true when loading', async () => {
streamController.enqueue('{"content": ');

userEvent.click(screen.getByTestId('submit-button'));

// wait for element "loading" to have text content "true":
await waitFor(() => {
expect(screen.getByTestId('loading')).toHaveTextContent('true');
});

streamController.enqueue('"Hello, world!"}');
streamController.close();

await screen.findByTestId('loading');
expect(screen.getByTestId('loading')).toHaveTextContent('false');
});
});

describe('when the API returns a 404', () => {
beforeEach(async () => {
mockFetchError({ statusCode: 404, errorMessage: 'Not found' });
Expand Down

0 comments on commit 321a7d0

Please sign in to comment.