From dae1e61490e8a94d6f43bda2bf16083a6de2e58b Mon Sep 17 00:00:00 2001 From: jhuleatt <3759507+jhuleatt@users.noreply.github.com> Date: Fri, 10 Sep 2021 09:58:42 -0400 Subject: [PATCH 1/5] add useCallableFunctionResponse --- docs/reference/README.md | 1 + docs/reference/modules/functions.md | 39 +++++++++++++++++++++++++++++ docs/reference/modules/index.md | 7 ++++++ docs/use.md | 17 +++++++++++-- src/functions.tsx | 28 +++++++++++++++++++++ src/index.ts | 1 + test/functions.test.tsx | 18 +++++++++++-- 7 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 docs/reference/modules/functions.md create mode 100644 src/functions.tsx diff --git a/docs/reference/README.md b/docs/reference/README.md index a206dabf..ea501d1a 100644 --- a/docs/reference/README.md +++ b/docs/reference/README.md @@ -11,6 +11,7 @@ ReactFire reference docs - [database](modules/database.md) - [firebaseApp](modules/firebaseApp.md) - [firestore](modules/firestore.md) +- [functions](modules/functions.md) - [index](modules/index.md) - [performance](modules/performance.md) - [remote-config](modules/remote_config.md) diff --git a/docs/reference/modules/functions.md b/docs/reference/modules/functions.md new file mode 100644 index 00000000..2e8bb795 --- /dev/null +++ b/docs/reference/modules/functions.md @@ -0,0 +1,39 @@ +[ReactFire reference docs](../README.md) / functions + +# Module: functions + +## Table of contents + +### Functions + +- [useCallableFunctionResponse](functions.md#usecallablefunctionresponse) + +## Functions + +### useCallableFunctionResponse + +▸ **useCallableFunctionResponse**<`RequestData`, `ResponseData`\>(`functionName`, `options?`): [`ObservableStatus`](../interfaces/useObservable.ObservableStatus.md)<`ResponseData`\> + +Calls a callable function. + +#### Type parameters + +| Name | +| :------ | +| `RequestData` | +| `ResponseData` | + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `functionName` | `string` | The name of the function to call | +| `options?` | [`ReactFireOptions`](../interfaces/index.ReactFireOptions.md)<`ResponseData`\> & { `data?`: `RequestData` ; `httpsCallableOptions?`: `HttpsCallableOptions` } | | + +#### Returns + +[`ObservableStatus`](../interfaces/useObservable.ObservableStatus.md)<`ResponseData`\> + +#### Defined in + +src/functions.tsx:13 diff --git a/docs/reference/modules/index.md b/docs/reference/modules/index.md index a23b5d59..73106a7d 100644 --- a/docs/reference/modules/index.md +++ b/docs/reference/modules/index.md @@ -36,6 +36,7 @@ - [useAnalytics](index.md#useanalytics) - [useAppCheck](index.md#useappcheck) - [useAuth](index.md#useauth) +- [useCallableFunctionResponse](index.md#usecallablefunctionresponse) - [useDatabase](index.md#usedatabase) - [useDatabaseList](index.md#usedatabaselist) - [useDatabaseListData](index.md#usedatabaselistdata) @@ -277,6 +278,12 @@ Re-exports: [useAuth](sdk.md#useauth) ___ +### useCallableFunctionResponse + +Re-exports: [useCallableFunctionResponse](functions.md#usecallablefunctionresponse) + +___ + ### useDatabase Re-exports: [useDatabase](sdk.md#usedatabase) diff --git a/docs/use.md b/docs/use.md index 0b27157b..dc7e831b 100644 --- a/docs/use.md +++ b/docs/use.md @@ -14,7 +14,8 @@ * [Show a single document](#show-a-single-document) * [Show a list of data (collection)](#show-a-list-of-data-collection) - [Cloud Functions](#cloud-functions) - * [Call a function](#call-a-function) + * [Call a function based on user interaction](#call-a-function-based-on-user-interaction) + * [Call a function on render](#call-a-function-on-render) - [Realtime Database](#realtime-database) * [Show an object](#show-an-object) * [Show a list of data](#show-a-list-of-data) @@ -304,7 +305,7 @@ function FavoriteAnimals() { The following samples assume that `FirebaseAppProvider` and `FunctionsProvider` components exist higher up the component tree. -### Call a function +### Call a function based on user interaction ```jsx function Calculator() { @@ -327,6 +328,18 @@ function Calculator() { } ``` +### Call a function on render + +If you want to call a function when a component renders, instead of in response to user interaction, you can use the `useCallableFunctionResponse` hook. + +```jsx +function LikeCount({ videoId }) { + const { status, data: likeCount } = useCallableFunctionResponse('capitalizeText', { data: { videoId: videoId } }); + + return This video has {status === 'loading' ? '...' : likeCount} views; +} +``` + ## Realtime Database The following samples assume that `FirebaseAppProvider` and `RealtimeDatabaseProvider` components exist higher up the component tree. diff --git a/src/functions.tsx b/src/functions.tsx new file mode 100644 index 00000000..e859b463 --- /dev/null +++ b/src/functions.tsx @@ -0,0 +1,28 @@ +import { httpsCallable as rxHttpsCallable } from 'rxfire/functions'; +import { ReactFireOptions, useObservable, ObservableStatus } from './'; +import { useFunctions } from '.'; + +import type { HttpsCallableOptions } from 'firebase/functions'; + +/** + * Calls a callable function. + * + * @param functionName - The name of the function to call + * @param options + */ +export function useCallableFunctionResponse( + functionName: string, + options?: ReactFireOptions & { + httpsCallableOptions?: HttpsCallableOptions; + data?: RequestData; + } +): ObservableStatus { + const functions = useFunctions(); + const observableId = `functions:callableResponse:${functionName}:${JSON.stringify(options?.data)}:${JSON.stringify(options?.httpsCallableOptions)}`; + const obsFactory = rxHttpsCallable(functions, functionName, options?.httpsCallableOptions); + + //@ts-expect-error because RxFire doesn't make data optional. Remove when https://github.com/FirebaseExtended/rxfire/pull/34 is released. + const observable$ = obsFactory(options?.data); + + return useObservable(observableId, observable$, options); +} diff --git a/src/index.ts b/src/index.ts index ddc322a7..ab60a1fe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -52,6 +52,7 @@ export * from './auth'; export * from './database'; export * from './firebaseApp'; export * from './firestore'; +export * from './functions'; export * from './performance'; export * from './remote-config'; export * from './storage'; diff --git a/test/functions.test.tsx b/test/functions.test.tsx index 79fb08a6..21c399c6 100644 --- a/test/functions.test.tsx +++ b/test/functions.test.tsx @@ -1,9 +1,10 @@ import { initializeApp } from 'firebase/app'; import { getFunctions, connectFunctionsEmulator, httpsCallable } from 'firebase/functions'; import { FunctionComponent } from 'react'; -import { FirebaseAppProvider, FunctionsProvider, useFunctions } from '..'; +import { FirebaseAppProvider, FunctionsProvider, useFunctions, useCallableFunctionResponse } from '..'; import { baseConfig } from './appConfig'; import { renderHook } from '@testing-library/react-hooks'; +import { randomString } from './test-utils'; import * as React from 'react'; describe('Functions', () => { @@ -25,7 +26,7 @@ describe('Functions', () => { expect(functionsInstance).toBeDefined(); // `capitalizeText` function is in `functions/index.js` - const capitalizeTextRemoteFunction = httpsCallable(functionsInstance, 'capitalizeText'); + const capitalizeTextRemoteFunction = httpsCallable<{ text: string }, string>(functionsInstance, 'capitalizeText'); const testText = 'Hello World'; const { data: capitalizedText } = await capitalizeTextRemoteFunction({ text: testText }); @@ -33,4 +34,17 @@ describe('Functions', () => { expect(capitalizedText).toEqual(testText.toUpperCase()); }); }); + + describe('useCallableFunctionResponse', () => { + it('calls a function on render', async () => { + const testText = randomString(); + const { result, waitFor } = renderHook(() => useCallableFunctionResponse<{ text: string }, string>('capitalizeText', { data: { text: testText } }), { + wrapper: Provider, + }); + + await waitFor(() => result.current.status === 'success'); + + expect(result.current.data).toEqual(testText.toUpperCase()); + }); + }); }); From 64dbc000b77dd3d7f7380aaf42452ab73f1b8ea9 Mon Sep 17 00:00:00 2001 From: jhuleatt <3759507+jhuleatt@users.noreply.github.com> Date: Fri, 10 Sep 2021 10:00:20 -0400 Subject: [PATCH 2/5] update sample --- docs/use.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/use.md b/docs/use.md index dc7e831b..c18bbe6a 100644 --- a/docs/use.md +++ b/docs/use.md @@ -334,7 +334,7 @@ If you want to call a function when a component renders, instead of in response ```jsx function LikeCount({ videoId }) { - const { status, data: likeCount } = useCallableFunctionResponse('capitalizeText', { data: { videoId: videoId } }); + const { status, data: likeCount } = useCallableFunctionResponse('countVideoLikes', { data: { videoId: videoId } }); return This video has {status === 'loading' ? '...' : likeCount} views; } From fb79788c1abf8a639edae870fafc063f58be88d2 Mon Sep 17 00:00:00 2001 From: Jeff <3759507+jhuleatt@users.noreply.github.com> Date: Fri, 10 Sep 2021 14:06:51 +0000 Subject: [PATCH 3/5] fix docs --- docs/reference/modules/functions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/modules/functions.md b/docs/reference/modules/functions.md index 2e8bb795..079f5fe7 100644 --- a/docs/reference/modules/functions.md +++ b/docs/reference/modules/functions.md @@ -36,4 +36,4 @@ Calls a callable function. #### Defined in -src/functions.tsx:13 +[src/functions.tsx:13](https://github.com/FirebaseExtended/reactfire/blob/main/src/functions.tsx#L13) From 256634beed4f563b0afa515b02f96d24ebb52215 Mon Sep 17 00:00:00 2001 From: Jeff <3759507+jhuleatt@users.noreply.github.com> Date: Fri, 10 Sep 2021 14:08:18 +0000 Subject: [PATCH 4/5] fix typo --- docs/use.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/use.md b/docs/use.md index c18bbe6a..6068e80d 100644 --- a/docs/use.md +++ b/docs/use.md @@ -336,7 +336,7 @@ If you want to call a function when a component renders, instead of in response function LikeCount({ videoId }) { const { status, data: likeCount } = useCallableFunctionResponse('countVideoLikes', { data: { videoId: videoId } }); - return This video has {status === 'loading' ? '...' : likeCount} views; + return This video has {status === 'loading' ? '...' : likeCount} likes; } ``` From c6d6bcdf228e8c6b621d58d3e0c4f863a2d52801 Mon Sep 17 00:00:00 2001 From: jhuleatt <3759507+jhuleatt@users.noreply.github.com> Date: Fri, 10 Sep 2021 10:17:06 -0400 Subject: [PATCH 5/5] add to example app --- example/withoutSuspense/Functions.tsx | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/example/withoutSuspense/Functions.tsx b/example/withoutSuspense/Functions.tsx index 7801889c..5ee1038b 100644 --- a/example/withoutSuspense/Functions.tsx +++ b/example/withoutSuspense/Functions.tsx @@ -1,7 +1,7 @@ import 'firebase/storage'; import * as React from 'react'; import { useState } from 'react'; -import { useFirebaseApp, FunctionsProvider, useFunctions } from 'reactfire'; +import { useFirebaseApp, FunctionsProvider, useFunctions, useCallableFunctionResponse } from 'reactfire'; import { CardSection } from '../display/Card'; import { LoadingSpinner } from '../display/LoadingSpinner'; import { WideButton } from '../display/Button'; @@ -13,11 +13,12 @@ function UpperCaser() { const [uppercasedText, setText] = useState(''); const [isUppercasing, setIsUppercasing] = useState(false); + const greetings = ['Hello World', 'yo', `what's up?`]; + const textToUppercase = greetings[Math.floor(Math.random() * greetings.length)]; + async function handleButtonClick() { setIsUppercasing(true); - const greetings = ['Hello World', 'yo', `what's up?`]; - const textToUppercase = greetings[Math.floor(Math.random() * greetings.length)]; const { data: capitalizedText } = await capitalizeTextRemoteFunction({ text: textToUppercase }); setText(capitalizedText); @@ -27,11 +28,23 @@ function UpperCaser() { return ( <> - {isUppercasing ? : {uppercasedText}} + {isUppercasing ? : {uppercasedText || `click the button to capitalize "${textToUppercase}"`}} ); } +function UpperCaserOnRender() { + const greetings = ['Hello World', 'yo', `what's up?`]; + const textToUppercase = greetings[Math.floor(Math.random() * greetings.length)]; + const { status, data: uppercasedText } = useCallableFunctionResponse<{ text: string }, string>('capitalizeText', { data: { text: textToUppercase } }); + + if (status === 'loading') { + return ; + } + + return {uppercasedText}; +} + export function Functions() { const app = useFirebaseApp(); @@ -40,6 +53,9 @@ export function Functions() { + + + ); }