How to use service-worker background sync with typescript - reactjs

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;
}
}

Related

Recoil Duplicate atom key when using with Webpack Module Federation

I'm using Webpack Module Federation to create 2 React applications: host and child.
In the host, I create atoms.ts and selector.ts filed and I expose them via the plugin under the expose section:
exposes: {
"./atoms": "./src/recoil/atoms.ts",
"./selectors": "./src/recoil/selectors.ts",
}
Inside the child, I just consume that via the remotes section:
remotes: {
host: "host#http://localhost:3000/remoteEntry.js",
}
Then, in the code of the child I use that like that:
import {someSelector} from "host/selectors"
const val = useRecoilValue(someSelector);
It's working fine but I got this warning in the console:
Duplicate atom key "userAuthState". This is a FATAL ERROR in
production. But it is safe to ignore this warning if it occurred because of
hot module replacement.
Does anyone face that issue and know if it's really a problem or how we could hide the warning?
Another related q:
Is it ok that the host will contain <RecoilRoot> and also the child will contain <RecoilRoot> ? because I want both will manage their own state but also share atom/selectors.
Thanks!
Regarding your second question:
Yes, this is totally fine. The nested <RecoilRoot> will create its own context and every atom referenced below the second root will be independent from the upper root. This is also explained in the docs.
Regarding the first question: As the log states this is fine as long as it occurs during development. Sometimes during the hot module replacement recoil throws away atoms and reinstantiates them causing this duplication to happen internally.
But as long as this warning doesn't pop up in your production code, everything is fine.
Are you importing the atoms or the selectors in your host application using a local path?
You need to include in your host webpack config its own entrypoint as remote and import your atoms from 'host/atoms'
I think this could solve your issue.
You can ignore the warning output (if you can bare it), functionality is not affected.
Also, you can install the intercept-stdout package and add the following to next.config.js (outside of the exported configuration):
const intercept = require("intercept-stdout")
// safely ignore recoil warning messages in dev (triggered by HMR)
function interceptStdout(text) {
if (text.includes("Duplicate atom key")) {
return "";
}
return text;
}
if (process.env.NODE_ENV === "development") {
intercept(interceptStdout);
}
This way can omit the annoying warning in console.

How to use i18next on a custom Typescript library and a host at the same time?

I am making a typescript library that accepts an object and spits out an excel. This library is supposed to be used with several React apps. Each React app[Host] will provide an object and the custom library returns an excel.
All the React apps use i18next for translation. The custom library also needs to use its own set of translations so that also has i18next. I am trying to avoid passing the translation strings from the Host to my library.
Problem:
As soon as I call any function in my library, i18next in the library takes over the i18n of Host. This is expected, As we have two i18next.init() functions [One in my library and one in the host]. Whoever gets called last wins [Library in my case]. All the translation strings in the Host are noted to be missing [by the i18nxt of library] since the newly initialized i18next in the library can't see Host translations.
Question:
How can I approach this problem, if I need to keep i18n on both the library and host? I am expecting both to have their own translations.
I believe you won't need any code to understand the problem.
As I have updated my question, I have managed to get this working. In my case, I have multiple react host apps which use my library. Some hosts don't have i18next installed and some already have it as a dependency. I needed a way to detect whether i8next is initialized or not. If it is not initialized initialize it with the excel generation library resources. Below is the code where I am detecting and adding/updating the resources based on the status of i18next. For my library resources, I am using a namespace so there won't be a name collition. You can access your library resources like t("tkdirectequibuild:heading"). I had to specify the namespace along with key always, which is not that bad. I kept lang as optional as i18next can detect the language in case of an already instantiated host.
import i18next from "i18next";
import enResources from "./translations/en/translation.json";
import frResources from "./translations/fr/translation.json";
export function addExcelResource(lang?: string) {
if (i18next.isInitialized) {
console.info("i18Next Exists");
if (!i18next.hasResourceBundle("en", "tkdirectequibuild")) {
console.info("en-Added");
i18next.addResourceBundle("en", "tkdirectequibuild", enResources);
}
if (!i18next.hasResourceBundle("fr", "tkdirectequibuild")) {
console.info("fr-Added");
i18next.addResourceBundle("fr", "tkdirectequibuild", frResources);
}
if (lang) {
i18next.changeLanguage(lang);
}
} else {
console.info("i18Next is missing in the HOST. Adding our own");
i18next.init({
lng: lang,
ns: ["tkdirectequibuild"],
initImmediate: false,
resources: {
en: {
tkdirectequibuild: enResources,
},
fr: {
tkdirectequibuild: frResources,
},
},
});
}
return i18next;
}
In the calling library function, I added these lines,
const i18n = addExcelResource(lang); const { t } = i18n;

Typescript type declarations for Promise that returns a React Component

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')));

React can't find TypeScript declarations module

I'm trying to use the react-csv-reader package in a React project that was created with create-react-app --typescript. Since react-csv-reader doesn't come with a types declaration file, I created one myself. I created a file types/react-csv-reader/index.d.ts. VS Code's Intellisense can find it just fine (I can command-click on the function name in module where I'm using react-csv-reader, and it takes me to my declarations file. It also complains when I don't have all the required props, etc.).
But when I run npm start, I get this:
Failed to compile.
./src/screens/ReadCsv.tsx
Module not found: Can't resolve '../types/react-csv-reader' in '/my/proj/root/src/screens'
Here's my index.d.ts:
import React from 'react'
interface CSVReaderProps {
cssClass?: string
cssInputClass?: string
label?: string
onFileLoaded: (data: any, filename: string) => void
onError: () => void
inputId?: string
inputStyle?: any
parserOptions?: any
}
declare const CSVReader: React.SFC<CSVReaderProps>
export default CSVReader
Because typescript don't know where are your definition files, so you have to tell him in your tsonfig.json.
{
"compilerOptions": {
"typeRoots" : [
"node_modules/#types",
"./your/types/folder"
]
}
}
Note that I added node_modules, otherwise its types are not included.
https://www.typescriptlang.org/docs/handbook/tsconfig-json.html#types-typeroots-and-types
The right combination of things to make this work was:
Put everything in index.d.ts inside a declare module 'react-csv-reader' {} block
import it as import CSVReader from 'react-csv-reader' instead of what I was doing, which was import CSVReader from '../types/react-csv-reader'.
I did not have to change anything in tsconfig.json. I still don't understand why it worked before as far as VS Code Intellisense was concerned, but this way works with that and is compiled happily by React.

NativeScript Typings

I'm working through the NativeScript getting started tutorial in TypeScript:
http://developer.telerik.com/featured/getting-started-nativescript/
In one snippet of code, I see:
exports.loadSignUpView = function(args) {
page = args.object;
page.bindingContext = journeyInfo;
}
After some research I was able to type args as
import app = require("application");
exports.loadSignUpView = function(args: app.ApplicationEventData) {
//...
}
But that still does not help me type the page object above, which has the bindingContext property. What is the TypeScript type that corresponds to the page?
Page type is defined in the "ui/page" module and the type of the args of the loaded event is EventData (from the "data/observable" module).
So you can do something like this:
import observable = require("data/observable");
import pages = require("ui/page");
// Event handler for Page "loaded" event attached in main-page.xml
export function loadSignUpView (args: observable.EventData) {
// Get the event sender
var page = <pages.Page>args.object;
}
Few more useful tips to get you started:
NativeScript has TypeScript support build in since the 1.5 release. You can now use the NativeScript CLI to setup typescript project. You can check the documentation for more.
In the documentation there is more up to date getting-started guide
All of the code snippets in the docs have also a TypeScript version so that you can see the typings there - we love typescript ;)

Resources