GithubHelp home page GithubHelp logo

andreiduca / use-async-resource Goto Github PK

View Code? Open in Web Editor NEW
94.0 3.0 9.0 476 KB

A custom React hook for simple data fetching with React Suspense

Home Page: https://dev.to/andreiduca/practical-implementation-of-data-fetching-with-react-suspense-that-you-can-use-today-273m

License: ISC License

JavaScript 19.84% TypeScript 80.16%
react reactjs async data fetch cache suspense hooks custom-hook react-hook

use-async-resource's People

Contributors

andreiduca avatar axelboc avatar dependabot[bot] avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

use-async-resource's Issues

Double endpoint fetching

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(), []);

imagen

If you need any clarification please ask. I've looked around the src but I haven't seen anything weird.
Thanks in advance

Error when using within a class component

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?

Invoking API calls directly in render?

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?

How to reset lazy function

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?

Function expression, which lacks return-type annotation, implicitly has an 'any' return type

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"
	]
}

useTransition doesn't seem to work with react 18 alpha.

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"

Data reader typing

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 a function that returns a UserType 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?

combining with import useErrorHandler of 'react-error-boundary' module

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

Update the data reader function immediately, and not in a useEffect hook

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.

Load more scenario

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 :)

Usage with react-router 6 `preload`

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?

How to add/remove an item to the `DataFn` that is temporarily in draft (not sync with server)

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

Pluggable cache strategies?

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:

  1. No cache. In a multi-user app with data being frequently modified, one might want to fetch fresh data from the backend whenever a page is displayed.
  2. Time-to-live setting. Some data might have logical lifespan, after which it could be automatically expired and thus saving us clearing the cache imperatively. The timeout might be set on the global level, per-wrapped fn, or per call.

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.

Get a `loading` property?

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?

useAsyncResource ArgTypes extends unknown[] too narrow

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?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.