@data-provider/core

Async Data Provider agnostic about data origins

Usage no npm install needed!

<script type="module">
  import dataProviderCore from 'https://cdn.skypack.dev/@data-provider/core';
</script>

README

Build status Coverage Status Quality Gate Mutation testing badge

Renovate Last commit Last release

NPM downloads License

Data Provider">

Async Data Provider. Powered by Redux. Agnostic about data origins. Agnostic about UI Frameworks.

Data Provider is a data provider (surprise!) with states and built-in cache for JavaScript apps.

The main target of the library are front-end applications, but it could be used also in Node.js.

It helps you providing async data to your components informing them about loading and error states. It also provides a cache layer, so you donĀ“t have to worry about when to read the data, and allows you to combine the results of different data providers using a syntax very similar to the known Reselect, recalculating them only when one of the dependencies cache is cleaned.

As its states are managed with Redux, you can take advantage of his large ecosystem of addons, which will improve the developer experience. (You don't need to use Redux directly in your application if you don't want, the library includes its own internal store for that purpose, which can be migrated to your own store easily for debugging purposes, for example).

You can use Data Provider with React, or with any other view library. Separated addons are available for that purpose, as @data-provider/react.

Data Provider is agnostic about data origins, so it can be used to read data from a REST API, from localStorage, or from any other origin. Choose one of the available addons depending of the type of the origin you want to read from, as @data-provider/axios, or @data-provider/browser-storage.

It has a light weight, 4.2KB gzipped in UMD format (you have to add the Redux weight to this), and addons usually are even lighter.

Docs

We have a website available to help you to learn to use Data Provider. There are tutorials, examples and many other resources to guide you to understand from the basic concepts to the more advanced patterns:

Main features

Agnostic about data origins

The Provider class provides the cache, state handler, etc., but not the read method. The read behavior is implemented by different Data Provider Origins addons.

There are different origins addons available, such as Axios, LocalStorage, Memory, etc. and building your own is so easy as extending the Provider class with a custom "readMethod".

Sharing the same interface for all origins, and being able to build Selectors combining all of them implies that your logic will be completely isolated about WHERE the data is being retrieved.

import { Axios } from "@data-provider/axios";
import { LocalStorage } from "@data-provider/browser-storage";

export const books = new Axios({
  id: "books",
  url: "/api/books"
});

export const favoriteBooks = new LocalStorage({
  id: "favorite-books",
  initialState: {
    data: []
  }
});

Selectors inspired by Reselect

Selectors cache is cleaned whenever any dependency cache is cleaned.

Exposing the same interface than providers make consumers agnostic about what type of Provider or Selector are they consuming.

As in Reselect, Selectors are composable. They can be used as input to other selectors.

Powerful dependencies api: Catch dependencies errors, retrieve them in parallel, declare them as functions returning other providers or selectors, etc.

import { Selector } from "@data-provider/core";

import { booksProvider } from "data/books";
import { authorsProvider } from "data/authors";

export const booksWithAuthor = new Selector(
  booksProvider,
  authorsProvider,
  (queryValue, books, authors) => {
    return books.map(book => ({
      ...book,
      author: authors.find(
        author => author.id === book.authorId
      )
    }))
  }
);

Cache and memoization

The built-in cache ensures that Providers are computed only once.

Don't care about when a data has to be retrieved. Simply retrieve it always, Data Provider will do the optimization. Avoid orchestrators and build fully modular pieces.

Cache can be cleaned on-demand, and some specific origins providers implementations even do it automatically when needed.

import Books from "views/books";

const RenderBooksTwice = () => {
  return (
    <div>
      <Books />
      <Books />
    </div>
  );
};

export default RenderBooksTwice;

Queryable

Providers and selectors instances can be queried, which returns a new child instance with its own query value.

Each different child has a different cache, different state, etc.

Different origins can use the queryValue for different purposes (API origins will normally use it for adding different params or query strings to the provider url, for example)

When the parent provider cache is clean, also the children is. (For example, cleaning the cache of an API origin requesting to "/api/books", will also clean the cache for "/api/books?author=2")

import { useData, useLoading } from "@data-provider/react";

import { bookProvider } from "data/books";
import BookCard from "components/book-card";

const Book = ({ id }) => {
  const provider = bookProvider.query({ id });
  const book = useData(provider);
  const loading = useLoading(provider);

  if (loading) {
    return <Loading />;
  }
  return <BookCard title={book.title} author={book.author} />;
};

export default Book;

UI binding addons

Data Provider is not concerned about the views, but UI binding addons are available.

For example, the @data-provider/react package gives you hooks to easily retrieve and provide data and other data-provider states to React components.

It also provides HOCs like "withData", "withLoading", etc. creating a wrapper component handling all the logic for you.

Optimized, it takes care of reading the data and re-renders the component only when the provider desired properties have changed. It also takes care of reading the data again every time the cache of the provider is invalidated.

import { useData, useLoading, useError } from "@data-provider/react";

import { booksProvider } from "data/books";
import ErrorComponent from "components/error";

const Books = () => {
  const error = useError(booksProvider);
  const data = useData(booksProvider);
  const loading = useLoading(booksProvider);

  if (error) {
    return <ErrorComponent error={error}/>
  }
  return <BooksList data={data} loading={loading} />;
};

export default Books;

Contributing

Contributors are welcome. Please read the contributing guidelines and code of conduct.