Skip to main content
Data Provider logo

Data Provider

JavaScript Async Data Provider

Cache and memoization

Optimized. Don't care about how many times do you call simultaneously to read a provider.

Data, loading and error states

Handling asynchronies implies handling error and loading states. Data Provider makes this job for you.

Selectors inspired by Reselect

Compute data derived from other Providers or Selectors with a familiar and very powerful syntax.

UI binding addons

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

The @data-provider/react package gives you hooks to easily retrieve and provide data and other data-provider states to React components: useData, useError, useLoading, useLoaded,...
It also provides HOCs like withData, withLoading, etc.

Optimized, it takes care of reading the data and re-renders the component only when your desired props have changed.

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;

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 available, such as Axios, LocalStorage, Memory, etc. and creating your own is easy. Read "creating origin addons" for further info.

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 from 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"
});

Selectors inspired by Reselect

Selectors are recomputed whenever any dependency changes.

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 queryValue. 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 cleaned, also the children are. (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;

Event emitter and Redux store

In most of cases, the integrations addons available (like @data-provider/react will save you having to interact directly with the providers, but, for most complex use cases you can listen to its events.

If this is not enough, as Data Provider uses Redux to handle providers states, it also provides an storeManager that allows to migrate it to your own store using combineReducers for debugging purposes, for example. Every single provider also has a getter for retrieving its own state directly.

import {
authorsProvider,
authorProvider
} from "data/authors";

authorsProvider.on("readStart", () => {
console.log("Authors request started");
});

authorProvider.onChild("*", eventName => {
if (["update", "delete", "create"].includes(eventName)) {
console.log("An author has been modified, cleaning cache");
authorsProvider.cleanCache();
}
});

Motivation

As a front-end developer and what some call an "architect", I've worked in very big projects, so I spent lot of time of last years trying to achieve a fully modular system in which the team could reuse pieces across many applications...