GithubHelp home page GithubHelp logo

dai-shi / react-suspense-fetch Goto Github PK

View Code? Open in Web Editor NEW
296.0 5.0 9.0 1.95 MB

[NOT MAINTAINED] A low-level library for React Suspense for Data Fetching

License: MIT License

TypeScript 85.28% JavaScript 14.72%
react reactjs react-suspense fetch rest-api

react-suspense-fetch's Introduction

This project is no longer maintained. React will soon introduce the use hook and cache. They cover what this project was trying to accomplish.


react-suspense-fetch

CI npm size discord

A low-level library for React Suspense for Data Fetching

Introduction

React 18 comes with Suspense (sort of), but Suspense for Data Fetching is left for data frameworks. The goal of this library is to provide a thin API to allow Suspense For Data Fetching without richer frameworks.

Project status: Waiting more feedbacks before finalizing the API.

Install

npm install react-suspense-fetch

Usage

import React, { Suspense, useState, useTransition } from 'react';
import { createRoot } from 'react-dom/client';

import { createFetchStore } from 'react-suspense-fetch';

// 1️⃣
// Create a store with an async function.
// The async function can take one input argument.
// The input value becomes the "key" of cache.
// By default, keys are compared with strict equal `===`.
const store = createFetchStore(async (userId) => {
  const res = await fetch(`https://reqres.in/api/users/${userId}?delay=3`);
  const data = await res.json();
  return data;
});

// 2️⃣
// Prefetch data for the initial data.
// We should prefetch data before getting the result.
// In this example, it's done at module level, which might not be ideal.
// Some initialization function would be a good place.
// We could do it in render function of a component close to root in the tree.
store.prefetch('1');

// 3️⃣
// When updating, wrap with startTransition to lower the priority.
const DisplayData = ({ result, update }) => {
  const [isPending, startTransition] = useTransition();
  const onClick = () => {
    startTransition(() => {
      update('2');
    });
  };
  return (
    <div>
      <div>First Name: {result.data.first_name}</div>
      <button type="button" onClick={onClick}>Refetch user 2</button>
      {isPending && 'Pending...'}
    </div>
  );
};

// 4️⃣
// We should prefetch new data in an event handler.
const Main = () => {
  const [id, setId] = useState('1');
  const result = store.get(id);
  const update = (nextId) => {
    store.prefetch(nextId);
    setId(nextId);
  };
  return <DisplayData result={result} update={update} />;
};

// 5️⃣
// Suspense boundary is required somewhere in the tree.
// We can have many Suspense components at different levels.
const App = () => (
  <Suspense fallback={<span>Loading...</span>}>
    <Main />
  </Suspense>
);

createRoot(document.getElementById('app')).render(<App />);

API

FetchStore

fetch store

prefetch will start fetching. get will return a result or throw a promise when a result is not ready. preset will set a result without fetching. evict will remove a result. abort will cancel fetching.

There are three cache types:

  • WeakMap: input has to be an object in this case
  • Map: you need to call evict to remove from cache
  • Map with areEqual: you can specify a custom comparator

Type: {prefetch: function (input: Input): void, get: function (input: Input, option: GetOptions): Result, preset: function (input: Input, result: Result): void, evict: function (input: Input): void, abort: function (input: Input): void}

Properties

  • prefetch function (input: Input): void
  • get function (input: Input, option: GetOptions): Result
  • preset function (input: Input, result: Result): void
  • evict function (input: Input): void
  • abort function (input: Input): void

createFetchStore

create fetch store

Parameters

  • fetchFunc FetchFunc<Result, Input>
  • cacheType CacheType<Input>?
  • presets Iterable<any>?

Examples

import { createFetchStore } from 'react-suspense-fetch';

const fetchFunc = async (userId) => (await fetch(`https://reqres.in/api/users/${userId}?delay=3`)).json();
const store = createFetchStore(fetchFunc);
store.prefetch('1');

Examples

The examples folder contains working examples. You can run one of them with

PORT=8080 npm run examples:01_minimal

and open http://localhost:8080 in your web browser.

You can also try them in codesandbox.io: 01 02 03 04 05 06 07

Blogs

react-suspense-fetch's People

Contributors

apostolos avatar dai-shi avatar dependabot[bot] avatar efraimrodrigues avatar wobsoriano 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  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

react-suspense-fetch's Issues

Support parallel fetches

The only way to do parallel fetches right now is to render two components as siblings and fetch the data there. Could this library support a getAll() helper that allows fetching multiple requests across the same store? What about multiple stores?

Single store could look something like this:

const userIds = [1,2,3];
const [user1, user2, user3] = getAll(userStore, userIds);

Multiple stores is a little more difficult. An API like this is a little less ergonomic than the above but would cover both cases:

const [foo, bar, baz] = getAll(
  () => fooStore.get(fooId),
  () => barStore.get(barId),
  () => bazStore.get(bazId),
);

An implementation that I got working looks like this, although I'm not sure of it's correctness

export const getAll = (...fns) => {
  const results = fns.map((fn) => {
    let result;
    try {
      result = { type: "ok", data: fn() };
    } catch (promise) {
      result = { type: "err", promise };
    }

    return result;
  });

  const data = [];
  for (const result of results) {
    if (result.type === "ok") {
      data.push(result.data);
    } else {
      throw result.promise;
    }
  }

  return data;
};

Strong caching for object inputs

If I understand correctly, the library keeps two caches: a "weak" cache (WeakMap) and a "strong" cache (Map). The weak cache is used for object inputs; the strong cache for primitive inputs.

I would really like to be able to pass an object to the store's methods while benefiting from the strong caching strategy.

Right now, I've written a little wrapper store that calls JSON.stringify (and JSON.parse on the way out) so that my input objects are transformed into strings in order to get the strong caching strategy. It works fine, but it'd be nice if createFetchStore could support it out of the box with some configuration options of sorts. The ideal would probably be to allow passing custom stringify and parse functions to pre- and post-process the inputs/results.

progress guessing

this isn't so much an issue as a request for new functionality in line with the fetch progress indicators project https://github.com/AnthumChris/fetch-progress-indicators so that I could pass to my Loading fallback how many bytes there were in all to get and how much was left. (or is that actually possible now, haven't gone through the code)

I can fork and make pull request on this if you want

Add entries to internal cache

I'm using a store to fetch entities organised in a tree structure. Each entity is identified by a path and can either be, let's say a folder or a file (but I don't know which one in advance).

To save network requests, when I fetch a folder, the response already includes the files it contains. What I'd like to do is to store those files in the store's internal cache against their respective paths, so that if they are ever requested directly, they do not get fetched again.

Right now, I'm achieving this behaviour by wrapping the fetch function I'm passing to the store and maintaining my own child cache:

const childCache = new Map<string, Entity>();

const store = createFetchStore(async (path: string) => {
  const cachedEntity = childCache.get(path);
  if (cachedEntity) {
    return cachedEntity;
  }

  const entity = await fetchEntity(path);

  if (isFolder(entity)) {
    // Cache children ...
    entity.children.forEach((child) => {
      // ... except sub-folders, since we do want those to be re-fetched fully (with their children)
      if (!isFolder(child)) {
        childCache.set(child.path, child);
      }
    });
  }

  return entity;
});

This code could be simplified a lot if I had a way to add entries to the store's internal cache. My initial thought was that store.prefetch() could perhaps receive a resolved value as second argument. 🤷

Infinite loop when input is an object

I'm running into an issue where get doesn't use the cache if the argument input is an object. It must be because the get method on WeakMap uses object identity, right? Is there any way around this? Serializing the object perhaps?

Project name

Is it necessary to use "suspense" in the name of the project? React suspense is still experimental, and having that in the name makes it harder for me to bring it up to my team, i.e. it seems to depend on an experimental feature, even though it is not.

feature: testing support / mocking

I am looking for any information how I could use this library in integration tests using Jest & React Testing Library. My goal is to be able to test screen using this library while avoiding real network calls but rather serving static data, e.g. from JSON.

`cache: stale-while-revalidate` support

Hey there -- thanks for this awesome example of a render-as-you-fetch library! I understand that it's experimental and new, but, it'd be really awesome to get support for refetching in the background while rendering with the latest fetched version that's available on the client. urql and apollo call this the stale-while-revalidate cache strategy: serve stale data to the react app while revalidating that it is fresh from the server.

I think the current API makes that kind of hard to implement in userland because a fetch is only triggered after an eviction, so you'd have to evict the current data to trigger a refetch, leaving you no data in the meantime to render with.

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.