Typescript type declarations for Promise that returns a React Component - reactjs

I have the following function:
const withCacheRefresh = (lazyLoadComponent: any) => {
return new Promise<React.ComponentType<any>>((resolve) => {
lazyLoadComponent()
.then(resolve)
.catch(() => {
window.location.reload(true);
});
});
}
It takes in a promise as an argument, specifically lazy. I proceed to call the promise, either resolving the promise by returning the result of lazy, or in my catch, refresh the page. I'm running into issues with the following Typescript compilation error:
Type 'Promise<ComponentType<any>>' is not assignable to type 'Promise<{ default: ComponentType<any>; }>'.
Type 'ComponentType<any>' is not assignable to type '{ default: ComponentType<any>; }'.
Property 'default' is missing in type 'ComponentClass<any, any>' but required in type '{ default: ComponentType<any>; }'. TS2322
I have two questions:
How can I specify my first argument lazyLoadComponent to be of the type for a function that returns a component of React.Component<any> type instead of just any?
How can I fix my Promise<React.ComponentType<any>> definition such that it conforms to the required type of Promise<{ default: ComponentType<any>; }> ?
Update
I've received answers telling me that my solution is incorrect for my stated problem, but I purposely chose to omit details about the nuances of my larger problem space.
Since it was asked, I feel like there's no pain clarifying: when new deployments get released for my application, new chunks get created, and the old ones purged from the production workspace. Consequently, clients that still remain in the previous release (since the app hasn't been refreshed to the latest version) are still using old chunks. HMR would not work since this is a production environment. An explicit refresh would cause the service workers to retrieve the latest chunks.
A similar circumstance is described in this Github Issue.

This is a convoluted (and slightly incorrect) way of doing things.
I'm going to assume you have a reason for reloading the page on an error other than "I hope it fixes the error" (if that's the reason or if it's for cache-busting purposes, you should definitely try HMR).
EDIT Since OP has clarified what he meant, I'd suggest, instead of hoping the browser doesn't cache the lazy-loaded component and thereby errors when a new version is rolled out, to use a precache manifest in the Service Worker itself, which would allow the SW to load the manifest whenever a new one is regestired. See this question for more info. You can integrate it into the bundler itself with the Workbox plugin, even if you don't use Workbox.
I'm also going to assume you want to use this with lazy, not call it on lazy, because otherwise it's not exactly possible to catch any errors.
import React from 'react';
type LazyFactory = Parameters<typeof React.lazy>[0];
const withCacheRefresh = (
importResult: LazyFactory
): LazyFactory => () =>
importResult().catch(() => {
window.location.reload(true);
// Following makes return type `never`, which satisfies TypeScript
throw new Error('component load failed');
});
There's no need to wrap it in a promise because it already is one. You can just .catch on it directly.
Usage:
const MyLazyComponent = React.lazy(withCacheRefresh(() => import('./Component')));

Related

What's the type of React Query's Error and how to handle different cases?

I'm using React Query with typescript to fetch data in my project and I'm trying to use the error the useQuery hook returns to display a message describing the error if it exists like this :
{isError && (error)? <p className=" text-[#ff0000] text-center font-semibold">{error?.response.data.message}</p> : null}
I'm having a problem with the error type :
How can I fix it, I couldn't find anything on the internet and if possible can anyone explain how to handle different thrown error with react query since you can throw anything in JS/TS
error defaults to type unknown because your queryFn can throw anything, that's a javascript behaviour. throw 5 will give you number as your error type. There is no good way to assert that, so unknown is the best possible type. It's also what TypeScript does now per default since v4.4 (useUnknownInCatchVariables)
The best way to make sure that your app runs as you want is to narrow the type at runtime. if (error instanceof Error) does the trick, and the you can safely access error.message.
If you are using axios, the type is actually an AxiosError, and axios has an axios.isAxiosError(error) helper that checks for that and also narrows the type correctly.
The alternative is to provide generics to useQuery:
useQuery<Todo[], AxiosError>(['todos'], fetchTodos)
but this is bad for a couple of reasons:
there are actually four generics, and you remove a bunch of features by only providing two of them
There is no guarantee that your error will really be an axios error. For example, if you use select, and a runtime error happens in there (because of some faulty logic), your error will be a "normal" Error, but not an axios Error.
Alas, as you can throw anything in JS, unknown is the most correct type.
I'm also going into more details in my blog:
https://tkdodo.eu/blog/react-query-and-type-script
As #TkDodo has pointed out, you could provide generics to useQuery but it's not worth it, since you will lose the Type inference too.
However, as a workaround, I add the onError callback and type its error arg instead. TypeScript will infer the type of that error to the type I am expecting from useQuery.
Note that I am using Axios request & response interceptors for all requests that I use to format and throw my custom errors.
Example...
interface IPayload {
someKey: string; // ETC
}
// The type of error expected from the response (also formatted by interceptor).
interface IApiError {
message: string;
description: string;
statusCode: string | number;
}
export const useMyQuery = (payload: IPayload) => {
const { data, isLoading, isError, error, refetch } = useQuery({
queryKey: ['some', 'query-keys'],
queryFn: () => API.fetchMyData(payload),
// This does the trick
onError: (err: IApiError) => err,
});
};
I had same issue with Typescript and react-query, same error Object is of type 'unknown'.
Installing this devDependency "#types/react-query" helped me somehow. I am using VS Code editor and I think that helped with type suggestions. This might help.
npm i --save-dev #types/react-query
For Mutation only:
If you are using Axios for Api calls, use the following to get Axios type errors:
const error = mutation.error as AxiosError;
Now, the error object will not throw any object not found warning.

WebStorm (IDEA) reports a TypeScript error when passing undefined into an optional prop

The problem
This started happening two days ago and I don't know how or why.
Whenever I have a component with an optional prop, Idea reports a TypeScript error when I try to instantiate it with a potentially undefined value - for example:
const Comp: React.FC<{ optional?: string }> = () => null
const renderComp = (optional?: string) => {
return <Comp optional={optional} />
// ^ Type string | undefined is not assignable to type string
}
I don't like this behaviour. The project still compiles fine when I run the server via CLI, but they flood my Problems IDE tab.
Can this be something akin to my TypeScript/IDE settings?
Some additional, potentially relevant info
One thing I noticed is that in my lowest IDE GUI ribbon, the version of TypeScript is not displayed! I am very sure that there has used to be a number next to it, for example TypeScript 4.4.4., but now the version is gone.
Here is a screenshot of my WebStorm TS config:
I also have recently upgraded WebStorm to version 2021.3.2 - I'm not sure if this is when these errors started appearing.

How to use service-worker background sync with typescript

I created a react project with TS (npx create-react-app my-app --template cra-template-pwa-typescript).
I followed the documentation : https://developers.google.com/web/updates/2015/12/background-sync). The following code is producing TS errors (even if it works):
// Register your service worker:
navigator.serviceWorker.register('/sw.js');
// Then later, request a one-off sync:
navigator.serviceWorker.ready.then(function(swRegistration) {
return swRegistration.sync.register('myFirstSync'); // error
});
self.addEventListener('sync', function(event) {
if (event.tag == 'syncSavedProjects') { // error
event.waitUntil(doSomeStuff()); // error
}
});
Property 'sync' does not exist on type 'ServiceWorkerRegistration'
Property 'tag' does not exist on type 'Event'
Property 'waitUntil' does not exist on type 'Event'
Looks like definitions are missing (lib.dom.d.ts)
There are a number of types that were removed from the standard type libraries in TypeScript v4.4.0, as described in this issue. Primarily, these are types related to web platform features that are not widely implemented (e.g. features that are Chrome-only at the moment). Background sync falls into that category.
The long-term plan is apparently, starting with TypeScript v4.5.0, to use standalone libraries like #types/web and #types/serviceworker, instead of the standard libraries that are updated with each TypeScript release. This means that in the future, upgrading your TypeScript dependency can be done independently from your type definition dependencies.
However, those libraries also have a policy of not including interfaces that are only available in a single browser. So I don't think that background sync is exposed in them either. There might be another #types/... library that does have them, but I have no been able to find them yet.
So, in terms of an actual solution today, the best approach I've found is to add a local copy of the old definitions to your TypeScript project. Here's a snippet of how we did this in the Workbox project for the background sync defintions:
interface SyncManager {
getTags(): Promise<string[]>;
register(tag: string): Promise<void>;
}
declare global {
interface ServiceWorkerRegistration {
readonly sync: SyncManager;
}
interface SyncEvent extends ExtendableEvent {
readonly lastChance: boolean;
readonly tag: string;
}
interface ServiceWorkerGlobalScopeEventMap {
sync: SyncEvent;
}
}

analytics().logEvent() return null and not tracking the event in console

I want to use firebase analytics in react native. I have added the lib,
but when i use the await analytics().logEvent("event_name") it returns null and event is also not added in firebase analytic console.
Any one have any idea what's wrong?
<Button
title="Add To Basket"
onPress={async () =>
await analytics().logEvent('basket', {
id: 3745092,
item: 'mens grey t-shirt',
description: ['round neck', 'long sleeved'],
size: 'L',
})
}
/>
I had the same issue when I started with analytics.
Firebase says it takes around 24 hours for the events to be visible on console.
Also the logEvent has a return type of void, hence you see null being returned. So don't worry regarding that aspect unless you have an error.
So, I would suggest you to wait for the changes to be reflected.
For Development purposes we would need to see Realtime events, and Firebase has DebugView for that. For details, refer this document:
https://firebase.google.com/docs/analytics/debugview

jsdom: Cannot read property '_location' of null

I'm getting test failed when I test epic via jest.
Test Failed
/.../node_modules/jsdom/lib/jsdom/browser/Window.js:148
return idlUtils.wrapperForImpl(idlUtils.implForWrapper(window._document)._location);
TypeError: Cannot read property '_location' of null
at Window.get location [as location] (/.../node_modules/jsdom/lib/jsdom/browser/Window.js:148:79)
at Timeout.callback [as _onTimeout] (/.../node_modules/jsdom/lib/jsdom/browser/Window.js:525:40)
at ontimeout (timers.js:380:14)
at tryOnTimeout (timers.js:244:5)
at Timer.listOnTimeout (timers.js:214:5)
And this is some of my test. I used fetch() and promise in fetchCategoriesEpic.
it('test epic', () => {
...
const action$ = ActionsObservable.of({ type: FETCH_CATEGORIES })
fetchCategoriesEpic(action$)
.toArray() // collects everything in an array until our epic completes
.subscribe((actions) => {
expect(actions).toEqual([
{ type: FETCH_CATEGORIES },
{ type: FETCH_CATEGORIES_SUCCESS, payload: List(payload.data).map((category) => new Category(category)) }
])
})
...
}
How can I solve this?
It appears this issues is really related to jsdom only, not redux-observable,
so you may want to change your title, description, tags. You'll probably want to include any configuration/usage of jsdom in your tests and what node version you're using.
Others have reported similar issues in the past but no clear solution I can find with a quick Google search. My gut would tell me your configuration is slightly off and it doesn't realize it should create a fake location object for you.
I ran into this issue when attempting to use document.documentElement while testing in an environment where document was undefined.
I added a conditional to that part of application code and my tests ran fine after that.
Make sure your application code rather than your test code is attempting to accessing ONLY elements that exist in your test environment.

Resources