Skip to content

(DOCSP-27000): Call a Function for React Native #2535

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Feb 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// :snippet-start: call-function
import React from 'react';
import {useUser} from '@realm/react';
// :remove-start:
import {Credentials} from 'realm';
import {useEffect, useState} from 'react';
import {App} from 'realm';
import {AppProvider, UserProvider, useApp} from '@realm/react';
import {render, fireEvent, waitFor} from '@testing-library/react-native';
import {View, Button, Text} from 'react-native';

const APP_ID = 'example-testers-kvjdy';

function AppWrapper() {
return (
<View>
<AppProvider id={APP_ID}>
<MyApp />
</AppProvider>
</View>
);
}

function MyApp() {
const [loggedIn, setLoggedIn] = useState(false);
const app = useApp();

useEffect(() => {
app.logIn(Credentials.anonymous()).then(user => user && setLoggedIn(true));
}, []);
// ...
return loggedIn ? (
<View>
<UserProvider>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can also provide a fallback component to the UserProvider. Then your wrapper above would look like so:

<AppProvider appId={}>
  <UserProvider fallback={<AnonLogin/>}>
      <MyApp/>
  </UserProvider>
</AppProvider>

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The UserProvider will not render its children until there is a current user set on the app object.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The UserProvider will not render its children until there is a current user set on the app object.

yeah it took me an hour+ of debugging to figure that out 😅

since this code isn't in the example i don't think we need to include that here.

tho when we @realm/reactify the auth pages, this pattern will be important to explain (cc @krollins-mdb)

<Text>Foo!</Text>
<Addition />
</UserProvider>
</View>
) : null;
}

let higherScopedSum: number;
// :remove-end:

function Addition() {
// Get currently logged in user
const user = useUser();

const addNumbers = async (numA: number, numB: number) => {
// Call Atlas Function

// Method 1: call with User.callFunction()
const sumMethod1 = await user?.callFunction('sum', numA, numB);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const sumMethod1 = await user?.callFunction('sum', numA, numB);
const sumMethod1 = await user.callFunction('sum', numA, numB);

I need to get a fix out for this immediately. The user from useUser should always be set and never undefined or null.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, i'd rather keep this example as is til that's fixed since the code will throw a TS error if missing ?


// Method 2: Call with User.function.<Function name>()
const sumMethod2 = await user?.functions.sum(numA, numB);

// Both methods return the same result
console.log(sumMethod1 === sumMethod2); // true
// :remove-start:
expect(sumMethod1).toBe(sumMethod2);
higherScopedSum = sumMethod1 as number;
// :remove-end:
};
// ...
// :remove-start:
return <Button onPress={() => addNumbers(1, 2)} testID='test-function-call' title='Test Me!' />;
// :remove-end:
}
// :snippet-end:

afterEach(async () => await App.getApp(APP_ID).currentUser?.logOut());

test('Call Atlas Function', async () => {
const {getByTestId} = render(<AppWrapper />);

const button = await waitFor(() => getByTestId('test-function-call'));
fireEvent.press(button);
await waitFor(() => expect(higherScopedSum).toBe(3));
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';
import {useUser} from '@realm/react';

function Addition() {
// Get currently logged in user
const user = useUser();

const addNumbers = async (numA: number, numB: number) => {
// Call Atlas Function

// Method 1: call with User.callFunction()
const sumMethod1 = await user?.callFunction('sum', numA, numB);

// Method 2: Call with User.function.<Function name>()
const sumMethod2 = await user?.functions.sum(numA, numB);
Comment on lines +14 to +15
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion, this would be the preferred syntax. But we need to show an example of how to type this. This can be done on the hook itself.

const user = userUser<FunctionTypes, CustomDataType, UserProfileDataType>;

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm confused by the code example and how it relates to calling a function. could you explain a bit more?

Copy link
Collaborator

@takameyer takameyer Feb 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you declare the FunctionTypes as so:

type FunctionTypes = {
  sum: (number, number) => number;
};

const Component = () => {
  const user = userUser<FunctionTypes>();
  
  const sum = user.functions.sum(12, 14); // <- this is now typed
  const sum = user.functions.foo(12, 14); // <- this will now give a type error
  const sum = user.functions.sum("foo", "bar"); // <- this will also now give a type error
}

Then sum will be typed for user.function.sum with typed arguments and a return value.

CustomDataType and UserProfileDataType can be used to add any extra user data and have that typed as well.

The thing is, it is currently not possible to infer what a user has configured in App Services, so they would have to define the types themselves in order to have proper typing in their application. Ideally we should have a way to generate this for our users, but for now, this is the way.

Copy link
Collaborator

@takameyer takameyer Feb 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not very well documented. There is an example of this in realm-web. The type is set on the creation of Realm.App and in turn is applied to any user object returned from that app instance.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it, thanks for clarifying. this is quite useful. will include in the docs.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, after spending a bit of time with this, it seems that the API is slightly different from what's in the above example.

these differences are:

  1. i need to provide all 3 types to useUser<FunctionTypes, CustomDataType, Realm.DefaultUserProfileData> to not cause a TS error.
  2. FunctionTypes must be a union with Realm.DefaultFunctionsFactory to work. for example:
type FunctionTypes = {
  sum: (a: number, b: number) => number;
} & Realm.DefaultFunctionsFactory;

for these reasons, i'd rather not include this in the docs yet, though i think longer-term it absolutely makes sense to include in the docs.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can see the working version with the types in this commit checkpoint: RN types on useUser.

going to revert those changes in the HEAD of the branch.


// Both methods return the same result
console.log(sumMethod1 === sumMethod2); // true
};
// ...
}
37 changes: 16 additions & 21 deletions source/sdk/react-native/app-services/call-a-function.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,31 +21,26 @@ named ``sum`` that takes two arguments, adds them, and returns the result:
return a + b;
};

.. _react-native-call-a-function-by-name:

Call a Function by Name
-----------------------
Prerequisites
-------------

.. include:: /includes/important-sanitize-client-data-in-functions.rst
Before You Begin
----------------

To call a function, you can either pass its name and arguments to
``User.callFunction()`` or call the function as if it was a method on the
:js-sdk:`User.functions <Realm.User.html#functions>` property.
#. In an App Services App, :ref:`define an Atlas Function <define-a-function>`.
#. In your client project, :ref:`initialize the App client <react-native-connect-to-mongodb-realm-backend-app>`.
#. Then, :ref:`authenticate a user <react-native-authenticate-users>` in your React Native project.

.. note:: Link a MongoDB Atlas Data Source

This example requires a Realm app with a linked
:ref:`Atlas data source <data-sources>`. Replace
``<appId>`` in the code with your App ID, which you can find in the
left navigation menu of the App Services UI.
.. _react-native-call-a-function-by-name:

.. literalinclude:: /examples/generated/node/call-a-function.snippet.call-a-function-by-name.js
:language: javascript

Call a Function
---------------

When you run the code sample, your output should resemble the following:
.. include:: /includes/important-sanitize-client-data-in-functions.rst

.. code-block:: none
To call a function, you can either pass its name and arguments to
:js-sdk:`User.callFunction() <Realm.User.html#functions>` or call the function
as if it were a method on the :js-sdk:`User.functions <Realm.User.html#functions>` property.

Using the "functions.sum()" method: the sum of 2 + 3 = 5
Using the "callFunction()" method: the sum of 2 + 3 = 5
.. literalinclude:: /examples/generated/react-native/ts/atlas-functions.test.snippet.call-function.tsx
:language: typescript