>;
+ if (!loggingIn) {
+ return null;
+ }
+
+ return (
+ Logging in, please wait a moment.
+ );
+}
```
-### useUserId() / withUserId(...)
+TypeScript signature:
-Get a stateful value of the current user id from [`Meteor.userId`](https://docs.meteor.com/api/accounts.html#Meteor-userId), a reactive data source.
+```ts
+function useLoggingIn(): boolean;
+```
+
+### useLoggingOut()
-The hook, `useUserId()`, returns a stateful value of the current user id.
+Get a stateful value of whether the logout method is currently in progress. A hook. Uses `Meteor.loggingOut` (no online documentation), a reactive data source.
- Arguments: *none*.
-- Returns: `string`.
+- Returns: `boolean`.
+
+Example:
+
+```tsx
+import React from 'react';
+import { useLoggingOut } from 'meteor/react-meteor-accounts';
+
+function Foo() {
+ const loggingOut = useLoggingOut();
+
+ if (!loggingOut) {
+ return null;
+ }
+
+ return (
+ Logging out, please wait a moment.
+ );
+}
+```
-The HOC, `withUserId(Component)`, returns a wrapped version of `Component`, where `Component` receives a prop of the current user id, `userId`.
+TypeScript signature:
+
+```ts
+function useLoggingOut(): boolean;
+```
+
+### withUser(...)
+
+Return a wrapped version of the given component, where the component receives a stateful prop of the current user record, `user`. A higher-order component. Uses [`Meteor.user`](https://docs.meteor.com/api/accounts.html#Meteor-user), a reactive data source.
- Arguments:
| Argument | Type | Required | Description |
| --- | --- | --- | --- |
-| Component | `any` | yes | A React component. |
+| Component | `React.ComponentType` | yes | A React component. |
- Returns: `React.ForwardRefExoticComponent`.
@@ -122,26 +190,46 @@ Examples:
```tsx
import React from 'react';
-import { useUserId, withUserId } from 'meteor/react-meteor-accounts';
+import { withUser } from 'meteor/react-meteor-accounts';
-// Hook
-function Foo() {
- const userId = useUserId();
+class Foo extends React.Component {
+ render() {
+ if (this.props.user === null) {
+ return Log in
;
+ }
- return (
-
-
Account Details
- {userId ? (
-
Your unique account id is {userId}.
- ) : (
-
Log-in to view your account details.
- )}
-
- );
+ return Hello {this.props.user.username}
;
+ }
}
-// HOC
-class Bar extends React.Component {
+const FooWithUser = withUser(Foo);
+```
+
+TypeScript signature:
+
+```ts
+function withUser(Component: React.ComponentType
): React.ForwardRefExoticComponent & Partial> & React.RefAttributes>;
+```
+
+### withUserId(...)
+
+Return a wrapped version of the given component, where the component receives a stateful prop of the current user id. A higher-order component. Uses [`Meteor.userId`](https://docs.meteor.com/api/accounts.html#Meteor-userId), a reactive data source.
+
+- Arguments:
+
+| Argument | Type | Required | Description |
+| --- | --- | --- | --- |
+| Component | `React.ComponentType` | yes | A React component. |
+
+- Returns: `React.ForwardRefExoticComponent`.
+
+Example:
+
+```tsx
+import React from 'react';
+import { withUserId } from 'meteor/react-meteor-accounts';
+
+class Foo extends React.Component {
render() {
return (
@@ -156,15 +244,89 @@ class Bar extends React.Component {
}
}
-const WrappedBar = withUserId(Bar);
+const FooWithUserId = withUserId(Foo);
+```
+
+TypeScript signature:
+
+```ts
+function withUserId
(Component: React.ComponentType
): React.ForwardRefExoticComponent & Partial> & React.RefAttributes>;
+```
+
+### withLoggingIn(...)
+
+Return a wrapped version of the given component, where the component receives a stateful prop of whether a login method (e.g. `loginWith`) is currently in progress. A higher-order component. Uses [`Meteor.loggingIn`](https://docs.meteor.com/api/accounts.html#Meteor-loggingIn), a reactive data source.
+
+- Arguments:
+
+| Argument | Type | Required | Description |
+| --- | --- | --- | --- |
+| Component | `React.ComponentType` | yes | A React component. |
+
+- Returns: `React.ForwardRefExoticComponent`.
+
+Example:
+
+```tsx
+import React from 'react';
+import { withLoggingIn } from 'meteor/react-meteor-accounts';
+
+class Foo extends React.Component {
+ render() {
+ if (!this.props.loggingIn) {
+ return null;
+ }
+
+ return (
+ Logging in, please wait a moment.
+ );
+ }
+}
+
+const FooWithLoggingIn = withLoggingIn(Foo);
```
TypeScript signatures:
```ts
-// Hook
-const useUserId: () => string;
+function withLoggingIn(Component: React.ComponentType
): React.ForwardRefExoticComponent & Partial> & React.RefAttributes>;
+```
+
+### withLoggingOut(...)
+
+Return a wrapped version of the given component, where the component receives a stateful prop of whether the logout method is currently in progress. A higher-order component. Uses `Meteor.loggingOut` (no online documentation), a reactive data source.
+
+- Arguments:
-// HOC
-const withUserId: (Component: any) => React.ForwardRefExoticComponent>;
+| Argument | Type | Required | Description |
+| --- | --- | --- | --- |
+| Component | `React.ComponentType` | yes | A React component. |
+
+- Returns: `React.ForwardRefExoticComponent`.
+
+Example:
+
+```tsx
+import React from 'react';
+import { withLoggingOut } from 'meteor/react-meteor-accounts';
+
+class Foo extends React.Component {
+ render() {
+ if (!this.props.loggingOut) {
+ return null;
+ }
+
+ return (
+ Logging out, please wait a moment.
+ );
+ }
+}
+
+const FooWithLoggingOut = withLoggingOut(Foo);
+```
+
+TypeScript signature:
+
+```ts
+function withLoggingOut(Component: React.ComponentType
): React.ForwardRefExoticComponent & Partial> & React.RefAttributes>;
```
diff --git a/packages/react-meteor-accounts/index.tests.ts b/packages/react-meteor-accounts/index.tests.ts
new file mode 100644
index 00000000..628be844
--- /dev/null
+++ b/packages/react-meteor-accounts/index.tests.ts
@@ -0,0 +1 @@
+import './react-accounts.tests.tsx';
diff --git a/packages/react-meteor-accounts/index.ts b/packages/react-meteor-accounts/index.ts
new file mode 100644
index 00000000..a7312bdf
--- /dev/null
+++ b/packages/react-meteor-accounts/index.ts
@@ -0,0 +1,21 @@
+import { Meteor } from 'meteor/meteor';
+import React from 'react';
+
+if (Meteor.isDevelopment) {
+ // Custom check instead of `checkNpmVersions` to reduce prod bundle size (~8kb).
+ const v = React.version.split('.').map(val => parseInt(val));
+ if (v[0] < 16 || (v[0] === 16 && v[1] < 8)) {
+ console.warn('react-accounts requires React version >= 16.8.');
+ }
+}
+
+export {
+ useUser,
+ useUserId,
+ useLoggingIn,
+ useLoggingOut,
+ withUser,
+ withUserId,
+ withLoggingIn,
+ withLoggingOut
+} from './react-accounts';
diff --git a/packages/react-meteor-accounts/package-lock.json b/packages/react-meteor-accounts/package-lock.json
index b4ae92e5..da1779fa 100644
--- a/packages/react-meteor-accounts/package-lock.json
+++ b/packages/react-meteor-accounts/package-lock.json
@@ -100,6 +100,36 @@
"@testing-library/dom": "^7.22.3"
}
},
+ "@testing-library/react-hooks": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-7.0.2.tgz",
+ "integrity": "sha512-dYxpz8u9m4q1TuzfcUApqi8iFfR6R0FaMbr2hjZJy1uC8z+bO/K4v8Gs9eogGKYQop7QsrBTFkv/BCF7MzD2Cg==",
+ "dev": true,
+ "requires": {
+ "@babel/runtime": "^7.12.5",
+ "@types/react": ">=16.9.0",
+ "@types/react-dom": ">=16.9.0",
+ "@types/react-test-renderer": ">=16.9.0",
+ "react-error-boundary": "^3.1.0"
+ },
+ "dependencies": {
+ "@babel/runtime": {
+ "version": "7.16.5",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.5.tgz",
+ "integrity": "sha512-TXWihFIS3Pyv5hzR7j6ihmeLkZfrXGxAr5UfSl8CHf+6q/wpiYDkUau0czckpYG8QmnCIuPpdLtuA9VmuGGyMA==",
+ "dev": true,
+ "requires": {
+ "regenerator-runtime": "^0.13.4"
+ }
+ }
+ }
+ },
+ "@tsconfig/recommended": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@tsconfig/recommended/-/recommended-1.0.1.tgz",
+ "integrity": "sha512-2xN+iGTbPBEzGSnVp/Hd64vKJCJWxsi9gfs88x4PPMyEjHJoA3o5BY9r5OLPHIZU2pAQxkSAsJFqn6itClP8mQ==",
+ "dev": true
+ },
"@types/aria-query": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.0.tgz",
@@ -192,6 +222,24 @@
"csstype": "^3.0.2"
}
},
+ "@types/react-dom": {
+ "version": "17.0.11",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.11.tgz",
+ "integrity": "sha512-f96K3k+24RaLGVu/Y2Ng3e1EbZ8/cVJvypZWd7cy0ofCBaf2lcM46xNhycMZ2xGwbBjRql7hOlZ+e2WlJ5MH3Q==",
+ "dev": true,
+ "requires": {
+ "@types/react": "*"
+ }
+ },
+ "@types/react-test-renderer": {
+ "version": "17.0.1",
+ "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-17.0.1.tgz",
+ "integrity": "sha512-3Fi2O6Zzq/f3QR9dRnlnHso9bMl7weKCviFmfF6B4LS1Uat6Hkm15k0ZAQuDz+UBq6B3+g+NM6IT2nr5QgPzCw==",
+ "dev": true,
+ "requires": {
+ "@types/react": "*"
+ }
+ },
"@types/underscore": {
"version": "1.10.24",
"resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.10.24.tgz",
@@ -433,6 +481,26 @@
"scheduler": "^0.19.1"
}
},
+ "react-error-boundary": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz",
+ "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==",
+ "dev": true,
+ "requires": {
+ "@babel/runtime": "^7.12.5"
+ },
+ "dependencies": {
+ "@babel/runtime": {
+ "version": "7.16.5",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.5.tgz",
+ "integrity": "sha512-TXWihFIS3Pyv5hzR7j6ihmeLkZfrXGxAr5UfSl8CHf+6q/wpiYDkUau0czckpYG8QmnCIuPpdLtuA9VmuGGyMA==",
+ "dev": true,
+ "requires": {
+ "regenerator-runtime": "^0.13.4"
+ }
+ }
+ }
+ },
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
diff --git a/packages/react-meteor-accounts/package.js b/packages/react-meteor-accounts/package.js
index 3fa1fb84..8facc24f 100644
--- a/packages/react-meteor-accounts/package.js
+++ b/packages/react-meteor-accounts/package.js
@@ -2,7 +2,7 @@
Package.describe({
name: 'react-accounts',
- summary: 'React hook for reactively tracking Meteor data',
+ summary: 'React hook for reactively tracking Meteor Accounts data',
version: '1.0.0-beta.1',
documentation: 'README.md',
git: 'https://github.com/meteor/react-packages',
@@ -10,8 +10,20 @@ Package.describe({
Package.onUse((api) => {
api.versionsFrom(['1.10', '2.3']);
- api.use('tracker');
- api.use('typescript');
- api.mainModule('react-accounts.tsx', ['client', 'server'], { lazy: true });
+ api.use(['accounts-base', 'tracker', 'typescript']);
+
+ api.mainModule('index.ts', ['client', 'server'], { lazy: true });
+});
+
+Package.onTest((api) => {
+ api.use([
+ 'accounts-base',
+ 'accounts-password',
+ 'tinytest',
+ 'tracker',
+ 'typescript',
+ ]);
+
+ api.mainModule('index.tests.ts');
});
diff --git a/packages/react-meteor-accounts/package.json b/packages/react-meteor-accounts/package.json
index 03667b32..4f38b650 100644
--- a/packages/react-meteor-accounts/package.json
+++ b/packages/react-meteor-accounts/package.json
@@ -1,10 +1,12 @@
{
"name": "meteor-react-accounts",
"scripts": {
- "make-types": "npx typescript *.tsx --jsx preserve --declaration --emitDeclarationOnly --outDir types"
+ "make-types": "npx typescript react-accounts.tsx --jsx preserve --declaration --emitDeclarationOnly --esModuleInterop --outDir types --strict"
},
"devDependencies": {
"@testing-library/react": "^10.0.2",
+ "@testing-library/react-hooks": "^7.0.2",
+ "@tsconfig/recommended": "^1.0.1",
"@types/meteor": "^1.4.42",
"@types/react": "^16.9.34",
"react": "16.13.1",
diff --git a/packages/react-meteor-accounts/react-accounts.tests.tsx b/packages/react-meteor-accounts/react-accounts.tests.tsx
new file mode 100644
index 00000000..834c8798
--- /dev/null
+++ b/packages/react-meteor-accounts/react-accounts.tests.tsx
@@ -0,0 +1,332 @@
+import { renderHook } from '@testing-library/react-hooks/dom';
+import { cleanup, render, waitFor } from "@testing-library/react";
+import { Accounts } from "meteor/accounts-base";
+import { Meteor } from "meteor/meteor";
+import { Tinytest } from "meteor/tinytest";
+import React from "react";
+import {
+ useLoggingIn,
+ useLoggingOut,
+ useUser,
+ useUserId,
+ withLoggingIn,
+ WithLoggingInProps,
+ withLoggingOut,
+ WithLoggingOutProps,
+ withUser,
+ withUserId,
+ WithUserIdProps,
+ WithUserProps,
+} from "./react-accounts";
+
+// Prepare method for clearing DB (doesn't need to be isomorphic).
+if (Meteor.isServer) {
+ Meteor.methods({
+ reset() {
+ Meteor.users.remove({});
+ },
+ });
+}
+
+if (Meteor.isClient) {
+ // fixture data
+ const username = "username";
+ const password = "password";
+
+ // common test actions
+ async function login() {
+ await new Promise((resolve, reject) => {
+ Meteor.loginWithPassword(username, password, (error) => {
+ if (error) reject(error);
+ else resolve();
+ });
+ });
+ }
+ async function logout() {
+ await new Promise((resolve, reject) => {
+ Meteor.logout((error) => {
+ if (error) reject(error);
+ else resolve();
+ });
+ });
+ }
+
+ // common test arrangements
+ async function beforeEach() {
+ // reset DB; must complete before creation to avoid potential overlap
+ await new Promise((resolve, reject) => {
+ Meteor.call("reset", (error, result) => {
+ if (error) reject(error);
+ else resolve(result);
+ });
+ });
+ // prepare sample user
+ await new Promise((resolve, reject) => {
+ Accounts.createUser({ username, password }, (error) => {
+ if (error) reject(error);
+ else resolve();
+ });
+ });
+ // logout since `createUser` auto-logs-in
+ await logout();
+ }
+
+ // NOTE: each test body has three blocks: Arrange, Act, Assert.
+
+ Tinytest.addAsync(
+ "Hooks - useUserId - has initial value of `null`",
+ async function (test, onComplete) {
+ await beforeEach();
+
+ const { result } = renderHook(() => useUserId());
+
+ test.isNull(result.current);
+ onComplete();
+ }
+ );
+
+ Tinytest.addAsync(
+ "Hooks - useUserId - is reactive to login",
+ async function (test, onComplete) {
+ await beforeEach();
+
+ const { result, waitForNextUpdate } = renderHook(() => useUserId());
+ // use `waitFor*` instead of `await`; mimics consumer usage
+ login();
+ await waitForNextUpdate();
+
+ test.isNotNull(result.current);
+ onComplete();
+ }
+ );
+
+ Tinytest.addAsync(
+ "Hooks - useUserId - is reactive to logout",
+ async function (test, onComplete) {
+ await beforeEach();
+ await login();
+
+ const { result, waitForNextUpdate } = renderHook(() => useUserId());
+ // use `waitFor*` instead of `await`; mimics consumer usage
+ logout();
+ await waitForNextUpdate();
+
+ test.isNull(result.current);
+ onComplete();
+ }
+ );
+
+ Tinytest.addAsync(
+ "Hooks - useLoggingIn - has initial value of `false`",
+ async function (test, onComplete) {
+ await beforeEach();
+
+ const { result } = renderHook(() => useLoggingIn());
+
+ test.isFalse(result.current);
+ onComplete();
+ }
+ );
+
+ Tinytest.addAsync(
+ "Hooks - useLoggingIn - is reactive to login starting",
+ async function (test, onComplete) {
+ await beforeEach();
+
+ const { result, waitForNextUpdate } = renderHook(() => useLoggingIn());
+ login();
+ // first update will be while login strategy is in progress
+ await waitForNextUpdate();
+
+ test.isTrue(result.current);
+ onComplete();
+ }
+ );
+
+ Tinytest.addAsync(
+ "Hooks - useLoggingIn - is reactive to login finishing",
+ async function (test, onComplete) {
+ await beforeEach();
+
+ const { result, waitForNextUpdate } = renderHook(() => useLoggingIn());
+ login();
+ await waitForNextUpdate();
+ // second update will be after login strategy finishes
+ await waitForNextUpdate();
+
+ test.isFalse(result.current);
+ onComplete();
+ }
+ );
+
+ Tinytest.addAsync(
+ "Hooks - useLoggingOut - has initial value of `false`",
+ async function (test, onComplete) {
+ await beforeEach();
+
+ const { result } = renderHook(() => useLoggingOut());
+
+ test.isFalse(result.current);
+ onComplete();
+ }
+ );
+
+ Tinytest.addAsync(
+ "Hooks - useLoggingOut - is reactive to logout starting",
+ async function (test, onComplete) {
+ await beforeEach();
+
+ const { result, waitForNextUpdate } = renderHook(() => useLoggingOut());
+ logout();
+ // first update will be while logout is in progress
+ await waitForNextUpdate();
+
+ test.isTrue(result.current);
+ onComplete();
+ }
+ );
+
+ Tinytest.addAsync(
+ "Hooks - useLoggingOut - is reactive to logout finishing",
+ async function (test, onComplete) {
+ await beforeEach();
+
+ const { result, waitForNextUpdate } = renderHook(() => useLoggingOut());
+ logout();
+ await waitForNextUpdate();
+ // second update will be after logout finishes
+ await waitForNextUpdate();
+
+ test.isFalse(result.current);
+ onComplete();
+ }
+ );
+
+ Tinytest.addAsync(
+ "Hooks - useUser - has initial value of `null`",
+ async function (test, onComplete) {
+ await beforeEach();
+
+ const { result } = renderHook(() => useUser());
+
+ test.isNull(result.current);
+ onComplete();
+ }
+ );
+
+ Tinytest.addAsync(
+ "Hooks - useUser - is reactive to login",
+ async function (test, onComplete) {
+ await beforeEach();
+
+ const { result, waitForNextUpdate } = renderHook(() => useUser());
+ // use `waitFor*` instead of `await`; mimics consumer usage
+ login();
+ await waitForNextUpdate();
+
+ test.isNotNull(result.current);
+ test.equal(
+ result.current.username,
+ username,
+ "Expected username to match"
+ );
+ onComplete();
+ }
+ );
+
+ Tinytest.addAsync(
+ "Hooks - useUser - is reactive to logout",
+ async function (test, onComplete) {
+ await beforeEach();
+ await login();
+
+ const { result, waitForNextUpdate } = renderHook(() => useUser());
+ // use `waitFor*` instead of `await`; mimics consumer usage
+ logout();
+ await waitForNextUpdate();
+
+ test.isNull(result.current);
+ onComplete();
+ }
+ );
+
+ // Since the HOCs wrap with hooks, the logic is already tested in 'Hooks' tests, and we only really need to test for prop forwarding. However, doing so for the "non-initial" case of all these values seems more prudent than just checking the default of `null` or `false`.
+
+ // :NOTE: these tests can be flaky (like 1 in 5 runs).
+
+ Tinytest.addAsync(
+ "HOCs - withUserId - forwards reactive value",
+ async function (test, onComplete) {
+ await beforeEach();
+ function Foo({ userId }: WithUserIdProps) {
+ // need something we can easily find; we don't know the id
+ return {Boolean(userId).toString()};
+ }
+ const FooWithUserId = withUserId(Foo);
+ const { findByText } = render();
+
+ login();
+
+ await waitFor(() => findByText("true"));
+ cleanup();
+ onComplete();
+ }
+ );
+
+ // :TODO: this is flaky, fails ~1 in 10
+ Tinytest.addAsync(
+ "HOCs - withUser - forwards reactive value",
+ async function (test, onComplete) {
+ await beforeEach();
+ function Foo({ user }: WithUserProps) {
+ return {user?.username || String(user)};
+ }
+ const FooWithUser = withUser(Foo);
+ const { findByText } = render();
+
+ login();
+
+ await waitFor(() => findByText(username));
+ cleanup();
+ onComplete();
+ }
+ );
+
+ Tinytest.addAsync(
+ "HOCs - withLoggingIn - forwards reactive value",
+ async function (test, onComplete) {
+ await beforeEach();
+ function Foo({ loggingIn }: WithLoggingInProps) {
+ return {loggingIn.toString()};
+ }
+ const FooWithLoggingIn = withLoggingIn(Foo);
+ const { findByText } = render();
+
+ login();
+
+ await waitFor(() => findByText("true"));
+ cleanup();
+ onComplete();
+ }
+ );
+
+ // :TODO: this is flaky, fails ~1 in 5
+ Tinytest.addAsync(
+ "HOCs - withLoggingOut - forwards reactive value",
+ async function (test, onComplete) {
+ await beforeEach();
+ function Foo({ loggingOut }: WithLoggingOutProps) {
+ return {loggingOut.toString()};
+ }
+ const FooWithLoggingOut = withLoggingOut(Foo);
+ const { findByText } = render();
+ await login();
+
+ logout();
+
+ await waitFor(() => findByText("true"));
+ cleanup();
+ onComplete();
+ }
+ );
+}
diff --git a/packages/react-meteor-accounts/react-accounts.tsx b/packages/react-meteor-accounts/react-accounts.tsx
index 95a92334..13516461 100644
--- a/packages/react-meteor-accounts/react-accounts.tsx
+++ b/packages/react-meteor-accounts/react-accounts.tsx
@@ -1,8 +1,12 @@
import { Meteor } from 'meteor/meteor'
import { Tracker } from 'meteor/tracker'
-import { useState, useEffect, forwardRef } from 'react'
+import React, { useState, useEffect, forwardRef } from 'react'
-export const useUserId = () => {
+/**
+ * Hook to get a stateful value of the current user id. Uses `Meteor.userId`, a reactive data source.
+ * @see https://docs.meteor.com/api/accounts.html#Meteor-userId
+ */
+export function useUserId() {
const [userId, setUserId] = useState(Meteor.userId())
useEffect(() => {
const computation = Tracker.autorun(() => {
@@ -15,29 +19,142 @@ export const useUserId = () => {
return userId
}
-export const withUserId = (Component) => (
- forwardRef((props, ref) => {
- const userId = useUserId();
- return
- })
-)
+export interface WithUserIdProps {
+ userId: string | null;
+}
+
+/**
+ * HOC to forward a stateful value of the current user id. Uses `Meteor.userId`, a reactive data source.
+ * @see https://docs.meteor.com/api/accounts.html#Meteor-userId
+ */
+export function withUserId(Component: React.ComponentType
) {
+ return forwardRef(
+ // Use `Omit` so instantiation doesn't require the prop. Union with `Partial` because prop should be optionally overridable / the wrapped component will be prepared for it anyways.
+ (props: Omit
& Partial, ref) => {
+ const userId = useUserId();
+ return (
+
+ );
+ }
+ );
+}
-export const useUser = () => {
- const [user, setUser] = useState(Meteor.user())
+/**
+ * Hook to get a stateful value of the current user record. Uses `Meteor.user`, a reactive data source.
+ * @see https://docs.meteor.com/api/accounts.html#Meteor-user
+ */
+export function useUser() {
+ const [user, setUser] = useState(Meteor.user());
useEffect(() => {
const computation = Tracker.autorun(() => {
- setUser(Meteor.user())
- })
+ let user = Meteor.user();
+ // `Meteor.user` returns `undefined` after logout, but that ruins type signature and test parity. So, cast until that's fixed.
+ if (user === undefined) {
+ user = null;
+ }
+ setUser(user);
+ });
return () => {
- computation.stop()
+ computation.stop();
+ };
+ }, []);
+ return user;
+}
+
+export interface WithUserProps {
+ user: Meteor.User | null;
+}
+
+/**
+ * HOC to get a stateful value of the current user record. Uses `Meteor.user`, a reactive data source.
+ * @see https://docs.meteor.com/api/accounts.html#Meteor-user
+ */
+export function withUser(Component: React.ComponentType
) {
+ return forwardRef(
+ (props: Omit
& Partial, ref) => {
+ const user = useUser();
+ return ;
}
- }, [])
- return user
+ );
+}
+
+/**
+ * Hook to get a stateful value of whether a login method (e.g. `loginWith`) is currently in progress. Uses `Meteor.loggingIn`, a reactive data source.
+ * @see https://docs.meteor.com/api/accounts.html#Meteor-loggingIn
+ */
+export function useLoggingIn(): boolean {
+ const [loggingIn, setLoggingIn] = useState(Meteor.loggingIn());
+ useEffect(() => {
+ const computation = Tracker.autorun(() => {
+ setLoggingIn(Meteor.loggingIn());
+ });
+ return () => {
+ computation.stop();
+ };
+ }, []);
+ return loggingIn;
+}
+
+export interface WithLoggingInProps {
+ loggingIn: boolean;
+}
+
+/**
+ * HOC to forward a stateful value of whether a login method (e.g. `loginWith`) is currently in progress. Uses `Meteor.loggingIn`, a reactive data source.
+ * @see https://docs.meteor.com/api/accounts.html#Meteor-loggingIn
+ */
+export function withLoggingIn(Component: React.ComponentType
) {
+ return forwardRef(
+ (
+ props: Omit
& Partial,
+ ref
+ ) => {
+ const loggingIn = useLoggingIn();
+ return (
+
+ );
+ }
+ );
}
-export const withUser = (Component) => (
- forwardRef((props, ref) => {
- const user = useUser();
- return
- })
-)
+/**
+ * Hook to get a stateful value of whether the logout method is currently in progress. Uses `Meteor.loggingOut`, a reactive data source.
+ */
+export function useLoggingOut(): boolean {
+ const [loggingOut, setLoggingOut] = useState(Meteor.loggingOut());
+ useEffect(() => {
+ const computation = Tracker.autorun(() => {
+ setLoggingOut(Meteor.loggingOut());
+ });
+ return () => {
+ computation.stop();
+ };
+ }, []);
+ return loggingOut;
+}
+
+export interface WithLoggingOutProps {
+ loggingOut: boolean;
+}
+
+/**
+ * HOC to forward a stateful value of whether the logout method is currently in progress. Uses `Meteor.loggingOut`, a reactive data source.
+ */
+export function withLoggingOut(Component: React.ComponentType
) {
+ return forwardRef(
+ (
+ props: Omit
& Partial,
+ ref
+ ) => {
+ const loggingOut = useLoggingOut();
+ return (
+
+ );
+ }
+ );
+}
diff --git a/packages/react-meteor-accounts/tsconfig.json b/packages/react-meteor-accounts/tsconfig.json
new file mode 100644
index 00000000..1cb0c5fb
--- /dev/null
+++ b/packages/react-meteor-accounts/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "extends": "@tsconfig/recommended/tsconfig.json",
+ "compilerOptions": {
+ "jsx": "preserve"
+ }
+}
\ No newline at end of file
diff --git a/packages/react-meteor-accounts/types/react-accounts.d.ts b/packages/react-meteor-accounts/types/react-accounts.d.ts
index c66598b5..0a90ab56 100644
--- a/packages/react-meteor-accounts/types/react-accounts.d.ts
+++ b/packages/react-meteor-accounts/types/react-accounts.d.ts
@@ -1,6 +1,57 @@
-///
import { Meteor } from 'meteor/meteor';
-export declare const useUserId: () => string;
-export declare const withUserId: (Component: any) => import("react").ForwardRefExoticComponent>;
-export declare const useUser: () => Meteor.User;
-export declare const withUser: (Component: any) => import("react").ForwardRefExoticComponent>;
+import React from 'react';
+declare module 'meteor/meteor' {
+ module Meteor {
+ function loggingOut(): boolean;
+ }
+}
+/**
+ * Hook to get a stateful value of the current user id. Uses `Meteor.userId`, a reactive data source.
+ * @see https://docs.meteor.com/api/accounts.html#Meteor-userId
+ */
+export declare function useUserId(): string | null;
+export interface WithUserIdProps {
+ userId: string | null;
+}
+/**
+ * HOC to forward a stateful value of the current user id. Uses `Meteor.userId`, a reactive data source.
+ * @see https://docs.meteor.com/api/accounts.html#Meteor-userId
+ */
+export declare function withUserId(Component: React.ComponentType
): React.ForwardRefExoticComponent & Partial> & React.RefAttributes>;
+/**
+ * Hook to get a stateful value of the current user record. Uses `Meteor.user`, a reactive data source.
+ * @see https://docs.meteor.com/api/accounts.html#Meteor-user
+ */
+export declare function useUser(): Meteor.User | null;
+export interface WithUserProps {
+ user: Meteor.User | null;
+}
+/**
+ * HOC to get a stateful value of the current user record. Uses `Meteor.user`, a reactive data source.
+ * @see https://docs.meteor.com/api/accounts.html#Meteor-user
+ */
+export declare function withUser(Component: React.ComponentType
): React.ForwardRefExoticComponent & Partial> & React.RefAttributes>;
+/**
+ * Hook to get a stateful value of whether a login method (e.g. `loginWith`) is currently in progress. Uses `Meteor.loggingIn`, a reactive data source.
+ * @see https://docs.meteor.com/api/accounts.html#Meteor-loggingIn
+ */
+export declare function useLoggingIn(): boolean;
+export interface WithLoggingInProps {
+ loggingIn: boolean;
+}
+/**
+ * HOC to forward a stateful value of whether a login method (e.g. `loginWith`) is currently in progress. Uses `Meteor.loggingIn`, a reactive data source.
+ * @see https://docs.meteor.com/api/accounts.html#Meteor-loggingIn
+ */
+export declare function withLoggingIn(Component: React.ComponentType
): React.ForwardRefExoticComponent & Partial> & React.RefAttributes>;
+/**
+ * Hook to get a stateful value of whether the logout method is currently in progress. Uses `Meteor.loggingOut`, a reactive data source.
+ */
+export declare function useLoggingOut(): boolean;
+export interface WithLoggingOutProps {
+ loggingOut: boolean;
+}
+/**
+ * HOC to forward a stateful value of whether the logout method is currently in progress. Uses `Meteor.loggingOut`, a reactive data source.
+ */
+export declare function withLoggingOut(Component: React.ComponentType
): React.ForwardRefExoticComponent & Partial> & React.RefAttributes>;