Questrar is a simple React request state tracker for managing states of requests.
yarn add questrar
npm install questrar
The state of an app request is necessary to user's interactivity and experience. Continually, App needs to fetch data from server or do a complex computation. It's important to let the user know if request is still loading, failed or successful. This can be repetitive and hard to work around with and you are likely going to need in every application which fetches data from server or anywhere else.
Questrar alleviates those needs into a simple <Request />
wrapper - with the help of
React's composability making it easy to control, track request,
show loading icons and failed messages and other UI transformations based on a state of specific request.
See API section
A request state is typed as
// @flow
type RequestState = {
id: RequestId,
pending: boolean,
success: boolean,
failed: boolean,
clean: boolean,
successCount: number, // default 0
failureCount: number, // default 0
message?: any | { title: any, body: any },
}
type RequestProp = {
data: RequestState,
actions: {
pending: (id: string | number, message?: any) => void, // set request state of id to pending
failed: (id: string | number, message?: any) => void, // set request state of id to failed
success: (id: string | number, message?: any) => void, // set request state of id to success
remove: (id: string) => void, //remove completely
clean: (id: string) => void, // set request as untouched
dirty: (id: string) => void, // set request as touched
}
}
A default request state (initial request state or provided when request state is not found by id)
const defaultRequestState: RequestState = {
id: 'newRequestId',
pending: false,
success: false,
failed: false,
successCount: 0,
failureCount: 0,
clean: true,
message: undefined
}
A requestState is simple to use in a React component by wrapping the component tree with the Provider
.
stateProvider
prop is required.
import { Provider } from 'questrar';
import type { StateProvider } from 'questrar';
import defaultRequestStateProvider from 'questrar/store';
const stateProvider: StateProvider = defaultRequestStateProvider();
export const AppMain = () => (
<Provider stateProvider={stateProvider}>
<App />
</Provider>
)
Now you can use the Request
component in any of your components anywhere deep, anyhow deep.
const App = () => (
<Request id="id">
<View />
</Request>
)
A Request
component requires an id which will be used to track a request.
An id is recommended to be a string
or number
.
Symbol
should be used carefully since Symbol()
creates a new different object every time.
A request id should be a bit unique to a particular subject context. For instance, if you have a user account which can have many queries to api, (fetch user, edit user, delete user, upload photo), its bearable not to use a single request id (which obviously may be userId) for all these actions on the user. Instead, you can just concat to do
const fetchProfileRequestId = `${userId}_fetch`;
const deleteUserId = `${userId}_delete`
The consequence of using same id looks like showing a pending delete when indeed user is uploading a photo.
You don't have to create a request state inside a component. You just provide an id. If the id has no requestState, a default one is returned but never saved. A request state is saved if there is an update action on the request state
import { Request } from 'questrar';
export const UserProfile = ({ userId, data }) => {
return (
<Request id={userId} pendOnMount >
<ProfileView data={data} />
</Request>
);
}
Where you explicitly want to access the request state object, you can use the inject
prop on Request
;
<Request id={userId} pendOnMount inject>
<ProfileView data={data} />
</Request>
inject
can be a boolean or a function with signature (request: RequestProp) => Object
which would be received by the underlying component.
export const UserProfile = ({ userId, data }) => {
const mapToButtonProps = (request: RequestProp) => {
return {
loading: request.data.pending,
disable: request.data.pending || request.data.success || request.data.failureCount > 5,//disable after 5 request failures
onClick: () => request.actions.success(request.data.id)
};
};
return (
<Request id={stringOrNumberId} inject={mapToButtonProps} >
<Button >Fetch Profile</Button>
</Request>
);
}
At some point where the Request
component isn't that helpful to use with your component,
you can try with the withRequest
HOC and expect the request
state as a prop.
import { withRequest } from 'questrar';
type Props = {
userId: string,
data: Object,
request: RequestProp
}
const UserProfile = ({ userId, data, request }: Prop) => {
if(request.data.dirty){
const requestId = request.data.id;
request.actions.remove(requestId)
}
return (
<div>
{request.data.failed && <Banner content={request.data.message} />}
{request.data.success && <ProfileView data={data} />}
<Button
disable={request.data.pending || request.data.success}
loading={request.data.pending}
>
Fetch User Profile
</Button>
</div>
);
};
export default withRequest({ id: (props) => props.userId })(UserProfile);
withRequest
takes these options:
type RequestComponentOptions = {
id?: RequestId | Array<RequestId> | (props: Props) => (RequestId | Array<RequestId>),
mergeIdSources?: boolean, // merge options.id and props.id (if there is any),
stateProvider: StateProvider, // this stateProvider overrides the Provider stateProvider
}
export default withRequest(options?: RequestComponentOptions)(Component)
withRequest
takes an optional single id or optional list of ids or optional function
that can convert the props of the wrapped component to a single id or list of ids.
withRequest
automatically takes options.id
(if there is) if props.id
is empty.
Thats to say props.id
has precedence over options.id
.
However, if you require props.id
and options.id
to merge, set mergeIdSources
to true.
This will merge props.id
and options.id
with no duplicates and
return an object typed as RequestMapProp
similar to RequestProp
.
type RequestMapProp = {
data: { [id: RequestId ]: RequestState }, //instead of data: RequestState
actions: {
pending: (id: string | number, message?: any) => void, //set request state of id to pending
failed: (id: string | number, message?: any) => void, //set request state of id to failed
success: (id: string | number, message?: any) => void
}
}
Any of props.id
or options.id
is optional. If no id is found, an empty object is returned
Note: If a single id is provided by
props.id
oroptions.id
or both(ifmergeIdSources
istrue
), aRequestProp
is returned instead ofRequestMapProp
Any feedback and PR contributions will be appreciated regardless.
- Inspired by Christian Kaps @akkie