Type argument at moment of use - reactjs

I'm trying to build a generic tool for handling the response to React Query useQuery requests in my app, and build this out using TypeScript, so that the data is correctly typed.
In a parent component, the use case would be something like this:
const queryResult = useQuery<MyResponseType, Error>('whatever');
return (
<ReactQueryLoader useQueryResult={queryResult}>
{data => {
// Data should be of MyResponseType type, but is unknown
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}}
</ReactQueryLoader>
);
I tried defining this ReactQueryLoader as so:
import React from "react";
import { UseQueryResult } from "react-query";
import { Loading } from "./Loading";
interface ReactQueryLoaderProps {
useQueryResult: UseQueryResult<unknown, Error>;
handleError?: (error: Error) => void;
children: (data: unknown) => JSX.Element;
}
export const ReactQueryLoader = ({
useQueryResult,
handleError = error => {
console.error(error);
throw new Error(error.message);
},
children,
}: ReactQueryLoaderProps): null | JSX.Element => {
if (useQueryResult.isError) {
handleError(useQueryResult.error);
return null;
}
if (useQueryResult.isLoading) {
return <Loading />;
}
return children(useQueryResult.data);
};
But when I inspect the data passed to the child, instead of being the type of the expected response data, it is unknown. I'm having trouble getting this to work, as someone with a mid-low-level understanding of TypeScript. I thought I could apply the same pattern as in this answer to another question I had, but I can't figure out how to do it in this context.
I tried this:
interface ReactQueryLoaderProps {
// ...
children: <T extends unknown>(data: T) => JSX.Element;
}
But got this in the parent element:
(parameter) data: T extends unknown
I also tried:
interface ReactQueryLoaderProps {
useQueryResult: UseQueryResult<T extends unknown, Error>;
But that provoked two errors:
Under the T: Cannot find name 'T'.
Under the comma: '?' expected.
Is this possible to do?

The ReactQueryLoader function needs to be generic here, as well as the props type.
That might look something like this:
interface ReactQueryLoaderProps<T extends UseQueryResult<unknown, Error>> {
useQueryResult: T;
handleError?: (error: Error) => void;
children: (data: T['data']) => JSX.Element;
}
And
export function ReactQueryLoader<T extends UseQueryResult<unknown, Error>>({
useQueryResult,
handleError,
children,
}: ReactQueryLoaderProps<T>): null | JSX.Element {
//...
}
Here ReactQueryLoader is now a generic function that captures the query result type as T and passes that to the generic props type ReactQueryLoaderProps.
ReactQueryLoaderProps then uses that type as the type of useQueryResult, and pulls the ['data'] property from that type for the type of the children function parameter.
See playground

Related

Limit type of inner function's argument based on passed in children's props

I have a Navigator component that renders a list of children, it also exposes a push function that allow you to pass in a scene name. The scene name needs to be the exact list of children name prop.
import * as React from 'react';
interface ComponentProps {
push: (scene: string) => void;
}
interface Route {
name: string;
component: ({ push }: ComponentProps) => JSX.Element;
}
function Route(_props: Route): null {
return null;
}
interface INavigator {
children: React.ReactElement<Route>[];
}
function Navigator({ children }: INavigator): JSX.Element {
const [stack, setStack] = React.useState<string[]>([children[0].props.name]);
const Child = children[0].props;
const handlePush = (newSceneName: string): void => {
setStack([...stack, newSceneName]);
};
return <Child.component push={handlePush} />;
}
function Home({ push }: ComponentProps): JSX.Element {
// works
push('Play');
// should fail with type error
// Type 'Foo' is not assignable to type 'Home' | 'Play'
push('Foo');
return <div>Home</div>;
}
function Play(): JSX.Element {
return <div>Play</div>;
}
function Main(): JSX.Element {
return (
<Navigator>
<Route name="Home" component={Home} />
<Route name="Play" component={Play} />
</Navigator>
);
}
Playground link
I asked a similar question here but I could not for the life of me get react components to work the same way.
I tried updating the Route interface to have a generic, but keep seeing errors like
Argument of type 'string' is not assignable to parameter of type 'T'.
interface Route<T extends string> {
name: T;
component: ({ push }: ComponentProps<T>) => JSX.Element;
}
The other option I thought about is in the push function, just do something like
const handlePush = (newSceneName: string): void => {
const possibleScenes = children.map(child => child.prop.name);
if (possibleScenes.includes(newSceneName) {
throw new Error('invalid scene name');
}
setStack([...stack, newSceneName]);
};
while this works, it does not allow type completion and type checking. So I'm wondering if there is a way to do this where the type of push is updated to
- (newSceneName: string) => void
+ (newSceneName: 'Play' | 'Home') => void
One other note: I'm not 100% tied to the current implementation of Navigator, so if there is a way I can get the types to work, more than happy to adjust the component.

incompatible function argument in component props

I have a component that takes a list of items, known to have an ID, and a function that filters those items.
The type with an ID is a generic type of item, that all items will have.
But more specific items will include other props.
type GenericItem = {
id: string;
}
type SpecificItem = {
id: string;
someOtherProp: boolean;
}
I also have a function type, that uses the generic type to operate on.
type GenericItemFunction = (item: GenericItem) => boolean;
I then have this component that uses the GenericItem and GenericItemFunction in its props.
type CompProps = {
fn: GenericItemFunction;
items: GenericItem[];
}
const Comp: React.FC<CompProps> = ({ fn, items }) => <></>;
When I try to use this component with the Specific type, I am getting errors saying I cannot use an implementation of GenericItemFunction because the types for item are not compatible.
const App = () => {
const items: SpecificItem[] = [];
const filter = (item: SpecificItem) => item.someOtherProp;
return (
<Comp
fn={filter} // error on `fn` prop
items={items}
/>
)
}
The typescript error I receive is:
Type '(item: SpecificItem) => boolean' is not assignable to type 'GenericItemFunction'.
Types of parameters 'item' and 'item' are incompatible.
Property 'someOtherProp' is missing in type 'GenericItem' but required in type 'SpecificItem'.
I guess I have two questions;
Firstly, Why is there a conflict when both types expect the id: string property?
Secondly, is there a more sane way to do something like this?
My first though was the type for item on the GenericItemFunction could be inferred from the value provided to the items prop in the App component.
But to be completely honest, I'm not sure how that would look...
My other thought was to have the Comp be a generic, but not show to use a react component that uses generics... Seems like jsx/tsx doesn't really support that syntax.
I expect something like this to throw all sorts of errors.
const Comp = <T extends GenericItem,>({ fn, items }) => <></>;
const App = () => {
return <Comp<SpecificType> />;
}
Finally, I did try this and there aren't any errors. But the downside is the type for items is inferred to be any.
type GenericItem = {
id: string;
}
type SpecificItem = {
id: string;
someOtherProp: boolean;
}
type GenericItemFunction <T> = (item: T) => boolean;
type CompProps <T extends GenericItem = any> = {
fn: GenericItemFunction<T>;
items: T[];
}
const Comp: React.FC<CompProps> = ({ fn, items }) => <></>;
const App = () => {
const items: SpecificItem[] = [];
const filter = (item: SpecificItem) => item.someOtherProp;
return (
<Comp
fn={filter}
items={items}
/>
)
}
Here's a link to the playground I've been using.
https://www.typescriptlang.org/play?#code/C4TwDgpgBA4hB2EBOBLAxgSWBAtlAvFAN4BQAkCgCYBcUAzsKvAOYDcJAviSaJFAMqQ0KAGbosuAsRJRZUKrQZM2MuXQD2OCAHlgAC2QAFJOrC0ARuvUAbCAEN47Lj3DQ4iVJmw4AYgFd4NGAUdXgoAB4AFQA+KQAKFG9aSIBKAljLG3tHbhc+AGFNMGNTOgjIqAgAD2x4SjL3ZHFvKQcQWMJSMhF4WkbPCV8AoJD4KOj2OXlvOmSAbQBdJxI0UIYoQpwzKAAleyCAOh988M3ikzA6Dqg4oigegBpp3DKONPxY8OjwgHoJ7lW8HWAEEwGB4u9YqQpoD1okXrRBBBhGIvLhFlJFpM5LDgPcUNZsEh4vCcIihKJmrhIc8cAcNFpdAYkCUwOxVLIkBBgH4kGE4hyphEzoKhXIevgiGJCcguGKxaS6JLFXL5X9BSlOEA
UPDATE:
Why did you use any as default for GenericItem type? Without this I believe it should properly infer Genericitem from GenericItemFunction. – tymzap
Removing the = any for the CompProps typedef causes errors in the Comp declaration...
type CompProps <T extends GenericItem> = {
fn: GenericItemFunction<T>;
items: T[];
}
const Comp: React.FC<CompProps> = ({ fn, items }) => <></>; // this line has the error
Generic type 'CompProps' requires 1 type argument(s).
Meaning, I still have to declare the type somewhere. Meaning I need to know the variation of the GenericItem type before I use the component.
SpecificItem is just a representation of the types that happen to overlap with the GenericItem typedef.
In most cases, the Comp wont know what type will actually be used and any doesn't give any useful information to the author.
I'm hoping for something like...
type CompProps <T extends GenericItem> = {
items: <T extends GenericItem>[];
fn: <infer from T>[];
}
But I'm not sure if this exists, or something like it.
:face_palm:
The CompProps using <T extends GenericType = any> is the correct way to do this.
type CompProps <T extends GenericItem = any> = {
items: T[];
filter: (item: T) => boolean;
}
const Comp: React.FC<CompProps> = (props) => <></>;
The magic is happening here:
const App = () => {
...
const filter = (item: SpecificItem) => item.someOtherProp;
...
}
The const Comp: React.FC<CompProps> doesn't care what the generic type is, hence = any. As long as it has at least the same shape as GenericType.
But inside the App component, where we are defining the method for the filter prop, we are declaring the SpecificItem typedef in the argument. Only that method uses the SpecificItem typedef. CompProps only cares it meets the required shape of GenericItem.
Hope that helps someone.
In most cases, the Comp wont know what type will actually be used and any doesn't give any useful information to the author.
To me, that means Comp is generic. So I would remove the = any on CompProps:
type CompProps<T extends GenericItem> = {
fn: GenericItemFunction<T>;
items: T[];
};
...and then define Comp as generic. There's probably a way to do that with React.FC, but personally I don't use React.FC so I don't know what it is. :-) React.FC doesn't do much of anything for you, and as some people point out, amongst other things it says your component accepts children whether it does or not. I prefer to be explicit about it when my components accept children.
So I'd define Comp like this:
const Comp = <T extends GenericItem>({ fn, items }: CompProps<T>) => {
// Just to show that `fn` and `items` play together:
const example = items.map(fn);
return <>{example}</>;
};
If you wanted it to have children, you can add them easily enough. Use string if you only want to support text, or React.Node if you want to allow just about anything:
// Does allow children:
type Comp2Props<T extends GenericItem> = {
fn: GenericItemFunction<T>;
items: T[];
children: React.ReactNode;
};
const Comp2 = <T extends GenericItem>({ fn, items, children }: Comp2Props<T>) => {
// Just to show that `fn` and `items` play together:
const example = items.map(fn);
return <>{example}</>;
};
Either way, it works well in your scenario:
const App = () => {
const items: SpecificItem[] = [];
const filter = (item: SpecificItem) => item.someOtherProp;
return <>
<Comp
fn={filter}
items={items}
/>
</>;
};
And the types mean it doesn't work in places you probably don't want it to:
const App2 = () => {
const genericItems: GenericItem[] = [];
const filterGeneric = (item: GenericItem) => item.id === "example"; // Silly definition, but...
const specificItems: SpecificItem[] = [];
const filterSpecific = (item: SpecificItem) => item.someOtherProp;
return <>
{/* Desirable error, `fn` can't handle `GenericItem`s, only `SpecificItem`s */}
<Comp
fn={filterSpecific}
items={genericItems}
/>
{/* Works, `fn` handles the supertype of the items, which is fine */}
<Comp
fn={filterGeneric}
items={specificItems}
/>
{/* Desirable error because `Comp` doesn't [now] support `children` */}
<Comp
fn={filterSpecific}
items={specificItems}
>
children here
</Comp>
{/* Works because `Comp2` does support `children`*/}
<Comp2
fn={filterSpecific}
items={specificItems}
>
children here
</Comp2>
</>;
};
Playground link

TypeScript error "not assignable to type 'IntrinsicAttributes'" for React component with extended discriminated union

Problem:
I have an array of objects whose interfaces all extend a base interface.
I want to map these objects onto a React component which will route to specialised components for each of the supported child interfaces -- but first, I want to map over the array and extend each object with an onClick handler, whose signature is a generic which I want to specialise to suit whichever child interface it's being mapped onto.
I've come up with a solution that looks as though it should work, but I can't shake this TS error: Type 'AWithClick<T>' is not assignable to type 'IntrinsicAttributes. I see plenty of references in SO and elsewhere to TS errors related to that interface, but none quite seems to apply here.
I checked my solution against this helpful article, and I think the main difference in my implementation is that I'm trying to extend items from the union with specialised onClicks, rather than defining the specialised onClicks in the individual interfaces in the original union. The array of objects comes from a store, and I'm basically mapping its entities to component props, so I want to keep component props interfaces separate from the store interfaces.
Steps to repro:
npx create-react-app repro --template typescript
Replace App.tsx with the following:
import React from 'react';
enum AType { 'as', 'an' }
interface A {
type: AType;
}
interface AS extends A {
type: AType.as;
value: string;
}
interface AN extends A {
type: AType.an;
value: number;
}
type AnyA = AS | AN;
type AWithClick<T extends AnyA> = T & { onClick: (value: T['value']) => void }
const ASComponent = (props: AWithClick<AS>) => <button onClick={() => props.onClick(props.value)}>{props.value}</button>;
const ANComponent = (props: AWithClick<AN>) => <button onClick={() => props.onClick(props.value)}>{props.value}</button>;
const TestComponent = <T extends AnyA>(props: AWithClick<T>) => {
switch (props.type) {
case AType.as: return <ASComponent {...props} />;
case AType.an: return <ANComponent {...props} />;
}
};
const withoutClicks = [
{ type: AType.as, value: 'AS!' } as AS,
{ type: AType.an, value: 1 } as AN,
];
const withClicks = withoutClicks.map(<T extends AnyA>(props: T) => ({
...props,
onClick: (value: T['value']) => { console.log(value) },
}));
const TestComponentMain = () =>
<div>
{withClicks.map(props => <TestComponent {...props} key={props.type} />)}
</div>
export default TestComponentMain;
Now npm start and you'll see the error Type 'AWithClick<T>' is not assignable to type 'IntrinsicAttributes
It seems that typescript can't quite follow the logic to know that you are refining the type adequately.
But TestComponent here does not need to be generic. You can simply declare your argument as the superset of what you support, and then refine the type with conditionals.
This works:
const TestComponent = (props: AWithClick<AnyA>) => {
switch (props.type) {
case AType.as: return <ASComponent {...props} />;
case AType.an: return <ANComponent {...props} />;
}
};
Playground
In general, when troubleshooting error message with generics, it's always good ask the question "does this really need to be generic?"

Type '({ items }: PropsWithChildren<TodoProps>) => Element[]' is not assignable to type 'FunctionComponent<TodoProps>'

I'm learning Typescript-react and I'm stuck in this error Type '({ items }: PropsWithChildren<TodoProps>) => Element[]' is not assignable to type 'FunctionComponent<TodoProps>' and I am lost on this.
Complete error:
Type '({ items }: PropsWithChildren<TodoProps>) => Element[]' is not assignable to type 'FunctionComponent<TodoProps>'.
Type 'Element[]' is missing the following properties from type 'ReactElement<any, string | ((props: any) => ReactElement<any, string | ... | (new (props: any) => Component<any, any, any>)> | null) | (new (props: any) => Component<any, any, any>)>': type, props, key
Link for code: sandbox repo.
Error happens on the declaration of TodoList function within the TodoList.tsx file.
Any help is appreciated. Cheers!
Code:
import React from "react";
interface Todo {
id: number;
content: string;
completed: boolean;
}
interface TodoProps {
items: Todo[];
}
// v------v here is error
const TodoList: React.FC<TodoProps> = ({ items }) => {
return items.map((item: Todo) => <div key={item.id}>{item.id}</div>);
};
export default TodoList;
Yeah, the error may sound a bit confusing - in essence it says that you can only return a single ReactElement or its equivalent JSX.Element in the function component definition, enforced by React.FC type.
React Fragments solve this limitation, so you can write TodoList in the following manner:
interface TodoProps {
items: Todo[];
}
const TodoList: React.FC<TodoProps> = ({ items }) => (
<React.Fragment>
{items.map((item: Todo) => (
<div key={item.id}>{item.id}</div>
))}
</React.Fragment>
);
Short form:
const TodoList: React.FC<TodoProps> = ({ items }) => (
<>
{items.map(({ id }) => <div key={id}>{id}</div>)}
</>
);
By the way: With pure JS, both class and function components can return multiple elements in an array as render output. Currently, TS has a type incompatibility for returned arrays in function components, so Fragments provide a viable workaround here (in addition to type assertions).
I've encountered a similar error. Eventually I noticed that I'd renamed the file incorrectly from .js to .ts instead of to .tsx when converting a component to a FunctionComponent with TypeScript.
I also got this error when I was trying to return children props from my Loading component like below.
const { loading, children } = props;
return loading ? <p>Loading ... </p> : children;
Then i realize that React is expecting only one return value(1 parent component) from its render method. Therefore I wrapped children props with React.Fragment which is denoted by <></> and that solves my problem. Below is my Loadingcomponent sample, hope that helps someone else.
import { FunctionComponent } from "react";
interface ILoadingProps {
loading: boolean;
}
export const Loading: FunctionComponent<ILoadingProps> = (props) => {
const { loading, children } = props;
return loading ? <p>Loading ... </p> : <>{children}</>;
};
My problem was that I had allowed a "TypeScript Quick Fix" in VSCode to add async to the main functional component interface.
const Welcome: React.FC<TProps> = async (props) => {
After removing it,
const Welcome: React.FC<TProps> = (props) => {
things were back to normal.

How to write type definitions for React HoCs

I have a higher order component that deals with Firestore data for me. I'm pretty new to typescript and I'm having trouble getting the types to work as I'd like them to.
Here are the full files + some extra ts definitions
I have a couple problems:
React.Component not inferring type definitions:
type WithFirestoreHoC<Props = {}> = (
config: WithFirestoreConfig<Props>,
) => (
WrappedComponent: ComponentType<WithFirestore & Props>,
) => ComponentClass<Props, { error: Error; queries: {}; loaded: boolean }>;
const withFirestore: WithFirestoreHoC = ({
queries,
props: propPickList,
loading: { delay = 200, timeout = 0 } = {},
}) => WrappedComponent =>
class WithFirestoreConnect extends Component { ... }
config and WrappedComponent are getting their type definitions (as WithFirestoreConfig + ComponentType<WithFirestore & Props>, respectively.
However, WithFirestoreConnect is not inferring that it should be ComponentClass<Props, { error: Error; queries: {}; loaded: boolean }>.
I wouldn't mind defining the state twice, but that doesn't help me with getting Props from type WithFirestoreHoC<Props = {}> to class WithFirestoreConnect extends Component<Props, { error: Error; queries: {}; loaded: boolean }> { ... } because it can't find Props.
How to create a dynamic pick list
Part of WithFirestoreConfig defines that the config object has a list of props that get passed on to WrappedComponent
WrappedComponent: ComponentType<WithFirestore & Props>,
should really be
WrappedComponent: ComponentType<WithFirestore & Pick<Props, config.propsPickList>,
Is there a way to tell typescript that what you provide in config.propsPickList will determine what props WrappedComponent should expect?
Inferring Firestore types
There are 2 types of Firestore query responses, those for Documents and those for Collections/Queries. It would be amazing if those could be defined in config.queries as something like this:
{ queries: {
docQuery: myDocument as DocumentReference<docDataType>,
collectionQuery: myDocument as CollectionReference<docDataType>,
} }
so WrappedComponent could know whether to expect a query or document data structure on the other end.
This seems super complex so I have a simpler example (it's a shortcut that creates a single subscription) here that would at least be a good stepping stone towards what I want:
export const withFirestoreDocument: <
DataType = firestore.DocumentData,
Props = {}
>(
query: FirestoreQueryable<DataType>,
) => (
WrappedComponent: ComponentType<DocumentSnapshotExpanded<DataType>>,
) => WithFirestoreHoC<Props> = query => WrappedComponent =>
withFirestore({ queries: { _default: query } })(
mapProps<
DocumentSnapshotExpanded<DataType> & Props,
{ _default: DocumentSnapshotExpanded<DataType> } & Props
>(({ _default, ...props }) => ({ ...props, ..._default }))(WrappedComponent),
);
However I'm stuck here because I can't get mapProp's type definitions to pull from the function's type defs... What's the right way to do this?
React.Component not inferring type definitions: Make Props a type parameter of the function instead of the type alias and then declare it when you define withFirestore.
How to create a dynamic pick list: Add a PL type parameter for the union of the elements of the pick list. This will do the right thing when you let TypeScript infer PL at a call site, though it's possible for callers to produce unsound behavior by specifying PL to be a union type including elements that are not in the actual list.
Inferring Firestore types: I'm not sure where you were going with withFirestoreDocument. You can do this with another Q type parameter and some mapped types and conditional types to generate the types of the injected props from Q.
Here is my revision of withFirestore.tsx with all the new features, some unrelated fixes to get it to compile in my environment, and an example added at the bottom (which should probably rather be in a separate file):
import * as React from 'react';
import { Component, ComponentClass, ComponentType } from 'react';
import {
DocumentReference,
Query,
CollectionReference,
DocumentSnapshotExpanded,
QuerySnapshotExpanded
} from './firemodel';
import { firestore } from 'firebase';
import { pick, forEach, isEqual, isFunction } from 'lodash';
import { expandDocSnapshot, expandQuerySnapshot } from 'modules/providers/util';
import SmartLoader from 'modules/atoms/SmartLoader';
type FirestoreQueryable<DataType> =
| DocumentReference<DataType>
| Query<DataType>
| CollectionReference<DataType>;
type FirestoryQueryableFunction<
DataType,
Props
> = (
firestore: firestore.Firestore,
props: Props,
) => Promise<FirestoreQueryable<DataType>>;
type QueryConfigEntry<Props> =
FirestoreQueryable<any> | FirestoryQueryableFunction<any, Props>;
type QueryConfig<Props> = {
[queryName: string]: QueryConfigEntry<Props>;
};
type FirestoreQueryableExpanded<Props, QE extends QueryConfigEntry<Props>> =
QE extends FirestoreQueryable<any> ? FirestoreQueryableExpanded1<QE> :
QE extends FirestoryQueryableFunction<any, Props> ? FirestoreQueryableExpanded1<ReturnType<QE>> : unknown;
type FirestoreQueryableExpanded1<QE extends FirestoreQueryable<any>> =
QE extends CollectionReference<infer DataType> | Query<infer DataType> ? QuerySnapshotExpanded<DataType> :
QE extends DocumentReference<infer DataType> ? DocumentSnapshotExpanded<DataType> : unknown;
interface WithFirestoreConfig<Props, PL extends keyof Props, Q extends QueryConfig<Props>> {
/** Object containing the queries to be provided to WrappedComponent.
* The queryName used is also the prop name the snapshot is passed in. */
queries: Q;
/** A list of props to whitelist passing to WrappedComponent.
* Configs without a list will whitelist all props */
props?: PL[];
/** Loading config items */
loading?: {
/** Number of ms after which to display the loading icon */
delay?: number;
/** Number of ms after which to display the timeout message */
timeout?: number;
};
}
type WithFirestoreHoC = <Props>() => <PL extends keyof Props, Q extends QueryConfig<Props>>(
config: WithFirestoreConfig<Props, PL, Q>,
) => (
WrappedComponent: ComponentType<WithFirestore<Props, Q> & Pick<Props, PL>>,
) => ComponentClass<Props, { error: Error; queries: {}; loaded: boolean }>;
const withFirestore: WithFirestoreHoC =
// An extra function call is needed so that callers can specify Props and
// still have PL and Q inferred. It can be removed when
// https://github.com/Microsoft/TypeScript/issues/10571 is implemented.
<Props extends {}>() =>
// Note: if `props` is not passed, there will be no inference for PL and it
// will default to its constraint, which is exactly the behavior we want as
// far as typing is concerned.
<PL extends keyof Props, Q extends QueryConfig<Props>>({
queries,
props: propPickList,
loading: { delay = 200, timeout = 0 } = {},
}: WithFirestoreConfig<Props, PL, Q>) => WrappedComponent =>
class WithFirestoreConnect extends Component<Props, { error: Error; queries: WithFirestore<Props, Q>; loaded: boolean }> {
subscriptions: {
[queryName: string]: ReturnType<FirestoreQueryable<any>['onSnapshot']>;
} = {};
state = {
error: null as Error,
queries: {} as WithFirestore<Props, Q>,
loaded: false,
};
componentDidMount() {
this.restartSubscription();
}
cancelSubscriptions = () => {
forEach(this.subscriptions, unsubscribe => unsubscribe());
this.subscriptions = {};
};
restartSubscription = () => {
// Open questions:
// - figuring out when all loaded (use a promise?)
this.cancelSubscriptions();
forEach(queries, async (q: QueryConfigEntry<Props>, key) => {
let ref: FirestoreQueryable<any>;
if (isFunction(q)) {
// The fact that this is an async/await means that we can
// create dependent queries within our FirestoreQueryableFunction
ref = await q(firestore(), this.props);
} else {
// Narrowing is not working for some reason.
ref = q as FirestoreQueryable<any>;
}
if (ref instanceof firestore.DocumentReference) {
this.subscriptions[key] = ref.onSnapshot(
snap => {
this.setState({
queries: Object.assign({}, this.state.queries, {[key]: expandDocSnapshot(snap)}),
});
},
err => {
console.error(JSON.stringify(err));
this.setState({ error: err });
this.cancelSubscriptions();
},
);
} else if (
ref instanceof firestore.CollectionReference ||
ref instanceof firestore.Query
) {
let ref2: {onSnapshot(os: (snap: firestore.QuerySnapshot) => void, oe: (err: Error) => void): () => void; } = ref;
this.subscriptions[key] = ref2.onSnapshot(
snap => {
this.setState({
queries: Object.assign({}, this.state.queries, {[key]: expandQuerySnapshot(snap)}),
});
},
err => {
console.error(JSON.stringify(err));
this.setState({ error: err });
this.cancelSubscriptions();
},
);
}
});
};
componentDidUpdate(prevProps: Props) {
if (!isEqual(this.props, prevProps)) {
this.restartSubscription();
}
}
componentWillUnmount() {
this.cancelSubscriptions();
}
render() {
if (!this.state.loaded || this.state.error) {
return (
<SmartLoader
error={this.state.error}
timeout={timeout}
delay={delay}
/>
);
}
const whitelistedProps = propPickList
? pick(this.props, propPickList)
: this.props;
// Unsure what's wrong here ~ Matt
let WrappedComponent2 = WrappedComponent as any;
return <WrappedComponent2 {...whitelistedProps} {...this.state.queries} />;
}
};
export type WithFirestore<Props, Q extends QueryConfig<Props>> = {
[queryName in keyof Q]: FirestoreQueryableExpanded<Props, Q[queryName]>;
}
export default withFirestore;
// EXAMPLE
interface MyDoc {
y: number
}
declare let myDocRef: DocumentReference<MyDoc>;
declare let myCollRef: CollectionReference<MyDoc>;
let wrapped = withFirestore<{x: string}>()({
queries: {
myDoc: myDocRef,
myColl: myCollRef
},
})((props) => { return <>{props.myDoc.data.y + props.myColl.docs[props.x].data.y}</>; });

Resources