andreiduca / use-async-resource Goto Github PK
View Code? Open in Web Editor NEWA custom React hook for simple data fetching with React Suspense
License: ISC License
A custom React hook for simple data fetching with React Suspense
License: ISC License
Is there any recommended way to use useAsyncResource to change data in a way which is integrated with the suspense components?
I'm using Nswag (TS client generator) with use-async-resource.
I've noticed it fetches twice the same endpoint.
The API Client (auto generated) contains different services which are something like this.
export class ForgeService extends AuthorizedApiBase {
private http: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> };
private baseUrl: string;
protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;
constructor(configuration: IConfig, baseUrl?: string, http?: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> }) {
super(configuration);
this.http = http ? http : <any>window;
this.baseUrl = baseUrl !== undefined && baseUrl !== null ? baseUrl : "";
}
/**
* Gets all Forge Accounts
* @return A list of Forge Accounts
*/
getAllAccounts(signal?: AbortSignal | undefined): Promise<ForgeAccountDTO[]> {
let url_ = this.baseUrl + "/api/Forge/GetAllAccounts";
url_ = url_.replace(/[?&]$/, "");
let options_ = <RequestInit>{
method: "GET",
signal,
headers: {
"Accept": "application/json"
}
};
return this.transformOptions(options_).then(transformedOptions_ => {
return this.http.fetch(url_, transformedOptions_);
}).then((_response: Response) => {
return this.processGetAllAccounts(_response);
});
}
protected processGetAllAccounts(response: Response): Promise<ForgeAccountDTO[]> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : <ForgeAccountDTO[]>JSON.parse(_responseText, this.jsonParseReviver);
return result200;
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<ForgeAccountDTO[]>(<any>null);
}
}
I crate a instance of the service to make the calls.
export const forgeService = new ForgeService(APIConfig);
Then in the functional component if I just use
const [accountsReader, getAccounts] = useAsyncResource(forgeService.getAllAccounts, []);
It throws an error because "this" is undefined in line
let url_ = this.baseUrl + "/api/Forge/GetAllAccounts";
as if the method were static.
Using the next approach (with the instance) it makes the call twice.
const [accountsReader, getAccounts] = useAsyncResource(() => forgeService.getAllAccounts(), []);
If you need any clarification please ask. I've looked around the src but I haven't seen anything weird.
Thanks in advance
Hello,
maybe I just misunderstood the purpose of this library, since it has no examples related to class components; anyway, I'm trying to use it within a class component, doing something like:
export default class Demo extends React.Component {
constructor(props) {
super(props)
const [categoriesReader, getNewCategories] = useAsyncResource(fetchCategories, this.props.categoryId)
this.categoriesReader = categoriesReader
this.getNewCategories = getNewCategories
}
}
But I get the following error:
Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
Is it something expected?
There is no obvious reason for using object-hash
here and it adds 32kb to the bundle size.
In relation to #8 and #7 the useAsyncResource hook now synchronously invokes the passed-in fn. This means that all the examples on the home page effectively invoke API calls directly, synchronously from the component render() fn.
Is this not discouraged by React? Shouldn't we trigger side effects like initiating API calls from a useEffect hook instead? Or are we now supposed to do that ourselves in each component?
Hi,
is there a way to reset lazy function?
Say you have this fetcher
const [collectionsReader, getCollections] = useAsyncResource(fetchDataSourceCollections);
which fetchDataSourceCollections has one string argument.
Say you have called getCollections(string)
few times, but you need to reset collectionsReader
to undefined to set inital state of the fetcher.
Is there a way to reset to the initial state of the lazy function?
Hi,
I'm getting the following TS error with Typescript 3.9.5
node_modules/use-async-resource/src/useAsyncResource.ts:69:15 - error TS7011: Function expression, which lacks return-type annotation, implicitly has an 'any' return type.
69 return (() => undefined) as LazyDataOrModifiedFn<ResponseType>;
~~~~~~~~~~~~~~~
Found 1 error.
I'm using the following tsconfig.json
{
"compilerOptions": {
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es2015",
/* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "es2015",
/* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
"jsx": "react",
/* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
"declaration": true,
/* Generates corresponding '.d.ts' file. */
// "declarationMap": true,
/* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true,
/* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./lib",
/* Redirect output structure to the directory. */
// "rootDir": "./src",
/* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
"removeComments": true,
/* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
"downlevelIteration": true,
/* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
"isolatedModules": false,
/* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": false,
/* Enable all strict type-checking options. */
"noImplicitAny": true,
/* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true,
/* Enable strict null checks. */
// "strictFunctionTypes": true,
/* Enable strict checking of function types. */
// "strictBindCallApply": true,
/* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true,
/* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true,
/* Raise error on 'this' expressions with an implied 'any' type. */
"alwaysStrict": false,
/* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
"noUnusedLocals": true,
/* Report errors on unused locals. */
"noUnusedParameters": true,
/* Report errors on unused parameters. */
"noImplicitReturns": true,
/* Report error when not all code paths in function return a value. */
"noFallthroughCasesInSwitch": true,
/* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
"moduleResolution": "node",
/* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true,
/* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
"inlineSourceMap": true,
/* Emit a single file with source maps instead of having a separate file. */
"inlineSources": true,
/* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
/* Disallow inconsistently-cased references to the same file. */
},
"exclude": [
"./node_modules/*",
"lib/**/*",
"node_modules/**/*",
"test/**/*",
"**/__test__/*",
"**/*.spec.ts",
"src/**/*.test.*",
"src/**/*.spec.*"
],
"include": [
"src/**/*",
"index.ts"
]
}
I'm using the create root api and attempting to wrap the setter i get back from the hook in useTransition, however - the suspense boundary gets triggered immediately. I saw a blog post saying this feature was supported, but cant see it documented anywhere on github.
"react": "^18.0.0-alpha-327d5c484-20211106",
"react-dom": "^18.0.0-alpha-327d5c484-20211106",
"use-async-resource": "^2.2.2"
Great work with this package! It's really helpful! โญ
The readme states
Type inference for the data reader
The data reader will return exactly the type the original api function returns as a Promise.
const fetchUser = (userId: number): Promise<UserType> => fetch('...'); const [userReader] = useAsyncResource(fetchUser, 1);
userReader
is inferred as() => UserType
, meaning afunction
that returns aUserType
object.
In my experience, that is not entirely correct. Consider the following dummy code:
interface UserType {}
const fetchUser = (userId: number): Promise<UserType> => null as any;
const [userReader] = useAsyncResource(fetchUser, 1);
userReader
is then inferred to be of type DataOrModifiedFn<UserType>
, not () => UserType
.
If this change is intentional, the readme should be updated.
userReader
can still be cast to the mentioned type though:
const somePropsProperty: () => UserType = userReader;
which I would probably want to do when defining props interfaces for my components as I'd like to avoid exposing (the somewhat strange looking) DataOrModifiedFn
in them. Would you agree?
Hello,
is there a recommended way to combine this package with the useErrorHandler of 'react-error-boundary' module ?
It would be nice to provide a handler to catch fetch API errors and handle them with useErrorHandler
That would make the package great to handle both suspense and errorBoundary
thanks for your advices
Loic
Keeping the data reader in a state and updating it in a useEffect
hook when parameters change causes unnecessary re-renders. It also makes the useAsyncResource
hook less declarative, because it causes a moment in time where the data reader is not in sync with the parameters passed.
e.g.
expected:
// initial render
const [userReader] = useAsyncResource(fetchUser, 1);
userReader(); // => { id: 1, name: 'Alice' }
// second render: updated the id param to 2
const [userReader] = useAsyncResource(fetchUser, 2);
userReader(); // => { id: 2, name: 'Bob' }
// --> data reader should already be updated to be in sync with the params passed
actual:
// initial render
const [userReader] = useAsyncResource(fetchUser, 1);
userReader(); // => { id: 1, name: 'Alice' }
// second render: updated the id param to 2
const [userReader] = useAsyncResource(fetchUser, 2);
userReader(); // => { id: 1, name: 'Alice' }
// --> the hook still returns the old user object, as the update for the dataReader happens in a useEffect
// useEffect happens, updating the dataReader, causing a third render
const [userReader] = useAsyncResource(fetchUser, 2);
userReader(); // => { id: 2, name: 'Bob' }
// --> only now the data reader is in sync with the passed params
This problem can be avoided by keeping the data reader in a useRef
object and updating it BEFORE the render in a useMemo
hook.
I was using this library in a load more scenario (infinity scolling) - there does not seem to be a way to stop suspense being triggered causing a redraw when the getter is used to pull the next set of data - for example, when you want to add items to an array of rows in state.
If the library can handle this, some documentation for this specific scenario would be great or perhaps a modification to allow disabling of the suspense feature.
Currently the library works well for the initial load, but not for the load more deltas :)
Hey Andrei and thanks for the awesome library!
It's simple but powerful, I really like it.
I have a question regarding its usage with react-router
6.
Since alpha.5, react-router
supports the preload
function.
It seems like the perfect use case for use-async-resource
. The reader can be lazily initialized somewhere on top and then the api calls can be done in the preload
function.
This strategy worked for me, I was able to fetch in parallel both the React components and the api resources.
But I get this stacktrace from react:
react-dom.development.js:73 Warning: Cannot update a component (`App`) while rendering a different
component (`Routes`). To locate the bad setState() call inside `Routes`, follow the stack trace as
described in https://fb.me/setstate-in-render
in Routes (created by App)
in Router (created by BrowserRouter)
in BrowserRouter (created by App)
in Suspense (created by App)
in App
in StrictMode
printWarning @ react-dom.development.js:73
error @ react-dom.development.js:45
warnAboutRenderPhaseUpdatesInDEV @ react-dom.development.js:24697
scheduleUpdateOnFiber @ react-dom.development.js:22495
dispatchAction @ react-dom.development.js:16316
eval @ useAsyncResource.js:20
eval @ app.js:26
eval @ index.js:43
E @ index.js:43
Routes @ index.js:39
renderWithHooks @ react-dom.development.js:15273
mountIndeterminateComponent @ react-dom.development.js:18330
beginWork @ react-dom.development.js:19791
beginWork$1 @ react-dom.development.js:24635
performUnitOfWork @ react-dom.development.js:23540
workLoopConcurrent @ react-dom.development.js:23525
renderRootConcurrent @ react-dom.development.js:23490
performConcurrentWorkOnRoot @ react-dom.development.js:22768
workLoop @ scheduler.development.js:597
flushWork @ scheduler.development.js:552
performWorkUntilDeadline @ scheduler.development.js:164
I'm wondering now if I'm doing something wrong. I tried for both experimental
and latest
(16.13.1) react versions, all the same.
Here is how I do it:
App.js
:
...
const Categories = React.lazy(() => import(/* webpackChunkName: "categories" */ './components/categories'));
...
const fetchCategories = () =>
fetch('https://api.chucknorris.io/jokes/categories').then(r => r.json());
const App = () => {
const [categoriesReader, getCategories] = useAsyncResource(fetchCategories);
const preloadCategories = React.useCallback(() => {
getCategories();
}, []);
return (
<Suspense fallback={<Fallback />}>
<Router>
<Routes>
{/*...*/}
<Route
path="categories"
element={<Categories reader={categoriesReader} />}
preload={preloadCategories}
/>
{/*...*/}
</Routes>
</Router>
</Suspense>
);
and the component itself is:
import React from 'react'
const Categories = ({ reader }) => {
const data = reader();
if (data === undefined) return null;
return (
<ul>
{
data.map(item => <li key={item}>{item}</li>)
}
</ul>
)
}
export default Categories;
Can the warning be ignored? Or am I doing something wrong?
Could you please take a look?
For lazy initialization how about requiring that the user return a promise thunk instead of using an empty array? I.e. normal eager api:
const fetchUser = (id: number) => Promise<User>
Lazy api:
const saveUser=(user: User) => () => Promise<User>
This would also align it with fp-ts
task type: https://gcanti.github.io/fp-ts/modules/Task.ts.html
Hello there, we would like to edit the data that fetching back from somewhere in real world.
Is there any demo that shows how to add/remove an item to the list in draft (not sync with remote server)?
Take grafana for example, a dashboard contains many panels (fetched from server). We can insert a new panel or remove panels, and by clicking the save button the changes are sync with server.
Say, panels are all from panelsReader(), how to implement the add/remove button without making an api call (in draft) before clicking the save button?
function fetchPanels() {
return new Promise((resolve) => {
setTimeout(() => {
return [
{name: 'panel_1'},
{name: 'panel_2'},
]
})
});
}
function Panels(props) {
const panels = props.panelsReader();
function addPanel() {
// how to add item to panels that avoids api calls or being lost in re-render?
// some code here...
}
function savePanels() { /* ... sync with the server ... */ }
return (
<>
<button onClick={addPanel}>Add Panel</button>
<button onClick={savePanels}>Save</button>
{panels.map((panel) => (
<div>{panel.name}</div>
))}
</>
)
}
function Dashboard() {
const [panelsReader, updatePanelsReader] = useAsyncResource(fetchPanels, []);
return (
<Suspense fallback="loading...">
<Panels panelsReader={panelsReader} />
</Suspense>
)
}
Have you considered supporting different cache strategies?
Now the cache is hard-coded in the library, but providing a cache interface (or using some standard, existing one) that people can implement and then plug in their own cache might be useful. Some ideas of what could be handy:
Alternatively, it might be beneficial to use some existing, proven library for caching (e.g. https://www.npmjs.com/package/lru-cache ?) instead of implementing own solution, and allowing the users to configure it.
Hi, first of all thanks for this library, it's great.
I'm working with React Native and I would need a loading boolean to make ScrollView
RefreshControl
work.
I tried the useTransition
hook but it doesn't seem to do what I need.
const [refreshing, startTransition] = useTransition();
const refresh = useCallback(() => {
resourceCache(stuffLoader).clear();
startTransition(() => {
getStuff();
});
}, []);
I don't think I can get away with suspense in this case, is there any escape hatch to get a simple boolean?
Hi, i've got a particular use case where the return and arg types for my async function cannot be inferred due to them needing to be passed as generics when called.
I noticed that the useAsyncResource hook accepts 2 generics, the first being the return type and second being the params.
This is great, as i can pass my own generics when calling the hook, the only issue i have is that the generic for args extends unknown[]
.
My arg type is an object, not an array meaning i get a TS error. Is this by design? It seems to be that enforcing something that extends an array is a bit too narrow?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.