Handle modeling, fetching, and displaying remote data in React/Redux apps
A React library aimed at modeling, fetching, and displaying remote data and the states it can be in.
This library provides:
- api request wrapper based on Axios to make the HTTP requests
- fetchingReducer to update the store
- RemoteComponent to handle displaying remote data
These libraries are not bundled with remote-data but required at runtime:
npm i @alismael/remote-data
Performing a GET
request to fetch the data
actions.ts
import { api } from 'remote-data';
import { Post, ErrorResponse } from '../models';
import { FETCH_POSTS } from './constants';
const fetchPosts = () =>
api<Post[], ErrorResponse>({
method: 'GET',
url: 'posts',
baseURL: 'https://jsonplaceholder.typicode.com/',
action: FETCH_POSTS,
});
Adding a reducer to update the store
reducer.ts
import { Reducer } from 'react';
import { combineReducers } from 'redux';
import { fetchingReducer, RemoteData } from 'remote-data';
import { Post, ErrorResponse } from '../../models';
import { FETCH_POSTS } from './constants';
export type PostsStore = {
posts: RemoteData<Post[], ErrorResponse>;
};
const postsReducer: Reducer<PostsStore, any> = combineReducers({
posts: fetchingReducer<Post[], ErrorResponse>(FETCH_POSTS),
});
export default postsReducer;
Displaying your remote data
PostsComponent.tsx
const PostsLoading = () => <>Loading posts...</>;
const PostsError = ({ err }: { err: ErrorResponse }) => <>{err}</>;
const ListPosts = ({ data }: { data: Post[] }) => <>Here you can use the fetched data</>
type PostsContainerProps = {
fetchPosts: () => Promise<Post[]>;
posts: RemoteData<Post[], ErrorResponse>;
};
const PostsContainer = ({ fetchPosts, posts }: PostsContainerProps) => {
React.useEffect(() => {
fetchPosts();
}, [fetchPosts]);
return (
<RemoteComponent
remote={{ posts }}
loading={PostsLoading}
reject={({ posts }) => <PostsError error={posts.error} />}
success={({ posts }) => <ListPosts posts={posts.data} />}
/>
);
};
const mapStateToProps = ({ posts }: StoreState) => ({
posts: posts.posts,
});
const mapDispatchToProps = (
dispatch,
) => ({
fetchPosts: () => dispatch(fetchPostsAction()),
});
connect(mapStateToProps, mapDispatchToProps)(PostsContainer);
You can check the example
folder for more details
api<T, E>(config) where T, E are the types of data and the expected error respectively
import { api } from 'remote-data';
api<Post[], ErrorResponse>({
method: 'GET',
url: 'posts',
baseURL: 'https://jsonplaceholder.typicode.com/',
action: FETCH_POSTS,
});
Request Config
In addition to axios request config there are three more options:
action
: is the action type that will be dispatched when request state changed. If not provided no action will be dispatched.onSuccess
,onError
: are the callbacks to be triggered for the relevant request state.
fetchingReducer<T, E>(actionType) a reducer for managing the state of the remote data
import { fetchingReducer } from 'remote-data';
combineReducers({
posts: fetchingReducer<Post[], ErrorResponse>(FETCH_POSTS),
});
actionType
: it should be the same as the action passed to theapi
request wrapper
Handle displaying of your remote data.
import { RemoteComponent } from 'remote-data';
<RemoteComponent
remote={{ posts }}
loading={PostsLoading}
reject={({ posts }) => <PostsError error={posts.error} />}
success={({ posts }) => <ListPosts posts={posts.data} />}
/>
Only remote
and success
are required
remote
passing your remote data here, it should be of type RemoteData<T, E>loading
,success
, andreject
will be rendered for the relevant state
You can handle displaying multiple remote data at once with one component. here
RemoteData<T, E> where T
is the data type and E
is the error type respectively
enum RemoteKind {
NotAsked = 'NOT_ASKED',
Loading = 'LOADING',
Success = 'SUCCESS',
Reject = 'REJECT',
}
type NotAsked = {
kind: RemoteKind.NotAsked;
};
type Loading = {
kind: RemoteKind.Loading;
};
type Success<T> = {
kind: RemoteKind.Success;
data: T;
};
type Reject<E> = {
kind: RemoteKind.Reject;
error: E;
};
type RemoteData<T, E> = NotAsked | Loading | Success<T> | Reject<E>;
Action<T, E> where T
is the data type and E
is the error type respectively
type ActionType = string;
type NotAskedAction = {
type: ActionType;
kind: RemoteKind.NotAsked;
};
type LoadingAction = {
type: ActionType;
kind: RemoteKind.Loading;
};
type SuccessAction<T> = {
type: ActionType;
kind: RemoteKind.Success;
data: T;
headers: any;
};
type RejectAction<E> = {
type: ActionType;
kind: RemoteKind.Reject;
error: E;
headers: any;
};
type Action<T, E> =
| NotAskedAction
| LoadingAction
| SuccessAction<T>
| RejectAction<E>;
<RemoteComponent
remote={{ posts, users }}
loading={() => (
<>
<PostsLoading />
<UsersLoading />
</>
)}
reject={({ posts, users }) => (
<>
{users.error && <UsersError error={users.error} />}
{posts.error && <PostsError error={posts.error} />}
</>
)}
success={({ posts, users }) => (
<>
<h1 className="page-title">Users</h1>
<ListUsers users={users.data} />
<h1 className="page-title">Posts</h1>
<ListPosts posts={posts.data} />
</>
)}
/>
You can create your custom reducer, here's an example:
import { RemoteData, RemoteKind, Action } from 'remote-data';
import { Post, ErrorResponse } from '../../models';
import { FETCH_POSTS } from './constants';
export type PostsStore = {
posts: RemoteData<Post[], ErrorResponse>;
};
const initialState: PostsStore = {
posts: {
kind: RemoteKind.NotAsked,
},
};
export default (
state: PostsStore = initialState,
action: Action<Post[], ErrorResponse>,
): PostsStore => {
if (action.type === FETCH_POSTS) {
switch (action.kind) {
case RemoteKind.Loading:
return {
...state,
posts: {
kind: RemoteKind.Loading,
},
};
case RemoteKind.Success:
return {
...state,
posts: {
kind: RemoteKind.Success,
data: action.data,
},
};
case RemoteKind.Reject:
return {
...state,
posts: {
kind: RemoteKind.Reject,
error: action.error,
},
};
default:
return state;
}
}
return state;
};
- Initialize your state
- Verify the action type and kind
- Update your state
action
is of type Action<T, E>
To setup and run locally
- Clone this repo with
git clone https://github.com/alismael/remote-data
- Run
npm install
in the root folder - Run
npm install
in the example folder - run
npm start
in the root and example folders
MIT © alismael