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