nest-utilities-client-state
An extension for nest-utilities-client, providing an easy way to transition your HTTP services and data to reusable state through React hooks.
Installation
npm i @hulanbv/nest-utilities-client-state
Why?
Fetching data and updating user interfaces is a logic pattern we use all the time in our React apps. There are many ways of achieving a likewise pattern, think of simply fetching from `componentDidMount` or using a global state manager like Mobx or Redux. However you solve the problem, it will inevitably lead to a considerable amount of boilerplate code which has to be repeated every time data needs to be fetched -- let alone sharing data between multiple components.
This package provides a simpler way to manage data from a server (that utilizes nest-utilities), for apps that use nest-utilities-client.
Using data, fetch state, errors and more can be done in a single line of code. States from identical fetch requests are shared between components, making it possible for multiple live components to draw resources from the same state. Essentially functioning as a global state manager for remote data from your API.
How to use & examples
The packages provides a set of pre-made hooks regarding common use cases for persistant data (e.g. CRUD functions).
Hook | CRUD Function |
---|
useAll | GET |
useById | GET |
useDelete | DELETE |
useMany | GET |
usePatch | PATCH |
usePost | POST |
usePut | PUT |
Or, for edge cases: useRequest
.
All these hooks return an IRequestState object. The following example implements request state properties in a practical context.
Simple implementation
This react function component renders some user info.
function User({ id: string }) {
const { data } = useById(userService, id);
return (
<div data-id={data?.id}>
<p>{data?.firstName}</p>
<p>{data?.email}</p>
<p>{data?.dateOfBirth}</p>
</div>
);
}
Extensive example
This example renders a list of user mail adresses, and implements all provided state properties.
function EmailList() {
const { data, response, fetchState, call, cacheKey, service } = useAll(
userService,
{ limit: 10 },
{ cache: true }
);
useEffect(() => {
console.log(localStorage.getItem(cacheKey));
}, [cacheKey]);
if (fetchState === FetchState.Pending) return 'Loading...';
return response?.ok ? (
<div>
{/* Call the provided `call` method, which will (re)execute the fetch request. */}
<button onClick={() => call()}>{'Refresh data'}</button>
{/* Render your data */}
{data?.map((user) => (
<div key={user.id}>{user.email}</div>
))}
</div>
) : (
<p>Error: {response?.data.message}</p>
);
}
Custom hooks
Ofcourse, the provided hooks aren't restricted to usage directly in a React function component. React hooks were created for reusable state logic and this package adheres to that philosophy.
A useAll
implementation with some preset http options.
function usePopularArticles() {
return useAll(articleService, {
sort: ['-likesAmount'],
populate: ['author'],
limit: 10,
});
}
A custom authorization state manager
function useSession() {
const { data: sessionToken, response, call, service, cacheKey } = usePost(
authenticationService,
{ populate: ['user'] },
{ cache: 'authentication' }
);
const login = useCallback(
async (credentials: FormData) => await call(credentials as any),
[call]
);
const validate = useCallback(async () => await call(service.validate()), [
call,
]);
useEffect(() => {
const { token } = sessionToken;
}, [cacheKey, sessionToken]);
return {
login,
validate,
response,
sessionToken,
};
}
function App() {
const { login, sessionToken } = useSession();
if (sessionToken?.isActive) return (
<div>Hello, {sessionToken.user?.name}!</div>
);
return (
<form onSubmit={(e) => {
e.preventDefault();
login(new FormData(e.target);
}}>
<input name="username" />
<input name="password" />
</form>
);
}
Shared state
In this example, we have a Player list component, and a Status component. PlayerList
will display a list of available players, while Status
will display a welcome message, and the total amount of existing players.
Executing call
in component PlayerList
will also trigger an update in component Status
.
function PlayerList() {
const { data: players, call } = useAll(playerService, { select: ['id'] });
useEffect(() => {
setInterval(() => call(), 5000);
}, []);
return players?.map((player) => <div>{player.name}</div>);
}
function Status() {
const { response } = useAll(playerService, { select: ['id'] });
return (
<div>
<p>Total available players: {response?.headers.get('X-total-count')}</p>
</div>
);
}
function App() {
return (
<>
<Status />
<PlayerList />
</>
);
}
API reference
usePut
, usePatch
and useDelete
hooks will execute a proxy GET request, to get initial data to work with. So you won't have to create two hooks (for example useById + usePut) when you would want to fetch and edit data.
useAll(service, httpOptions, stateOptions)
Use a request state for all models. Will immediately fetch on creation, unless set otherwise in stateOptions
.
Arguments
service
: CrudService
httpOptions?
: IHttpOptions
stateOptions?
: IStateOptions
Returns
IRequestState
Example
const { data: animals } = useAll(animalService);
useById(service, id, httpOptions, stateOptions)
Use a request state for a single model, by model id. Will immediately fetch on creation, unless set otherwise in stateOptions
.
Arguments
service
: CrudService
id?
: string
httpOptions?
: IHttpOptions
stateOptions?
: IStateOptions
Returns
IRequestState
Example
const { data: car } = useById(carService, '<id>');
useDelete(service, id, httpOptions, stateOptions)
Use a request state for a single model that is to be deleted.
This method will not be called immediately on creation, but instead needs to be called with it's returned call
property to actually delete the model.
Arguments
service
: CrudService
id?
: string
httpOptions?
: IHttpOptions
stateOptions?
: IStateOptions
Returns
IRequestState
Example
const { data: covid19, call } = useDelete(pandemicService, '<id>');
const clickHandler = useCallback(() => {
call();
}, [call]);
useMany(service, ids, httpOptions, stateOptions)
Use a request state for a set of models, by id's. Will immediately fetch on creation, unless set otherwise in stateOptions
.
Arguments
service
: CrudService
ids
: Array<string>
httpOptions?
: IHttpOptions
stateOptions?
: IStateOptions
Returns
IRequestState
Example
const { data: guitars } = useMany(guitarService, ['<id1>', '<id2>']);
usePatch(service, id, httpOptions, stateOptions)
Use a request state to patch a model by id.
This method will not be called immediately on creation, but instead needs to be called with it's returned call
property to actually patch the model.
Arguments
service
: CrudService
id?
: string
httpOptions?
: IHttpOptions
stateOptions?
: IStateOptions
Returns
IRequestState
Example
const { call: patch } = usePatch(carService, '<id>');
const submitHandler = useCallback(
(formData: FormData) => {
patch(formData);
},
[call]
);
usePost(service, httpOptions, stateOptions)
Use a request state to create a model.
Arguments
service
: CrudService
httpOptions?
: IHttpOptions
stateOptions?
: IStateOptions
Returns
IRequestState
Example
const { call: create } = usePost(fruitService);
const submitHandler = useCallback(
(formData: FormData) => {
create(formData);
},
[create]
);
usePut
Use a request state to put a model by id.
This method will not be called immediately on creation, but instead needs to be called with it's returned call
property to actually update the model.
Arguments
service
: CrudService
id?
: string
httpOptions?
: IHttpOptions
stateOptions?
: IStateOptions
Returns
IRequestState
Example
const { call: put } = usePut(carService, '<id>');
const submitHandler = useCallback(
(formData: FormData) => {
put(formData);
},
[call]
);
useRequest(service, query, method, httpOptions, stateOptions)
Use a request state.
Arguments
service
: CrudService
query?
: string
method?
: "POST" | "GET" | "PUT" | "PATCH" | "DELETE", default "GET"
httpOptions?
: IHttpOptions
stateOptions?
: IStateOptions
Returns
IRequestState
Example
const { call, service } = useRequest(authenticationService, '', 'POST');
const login = useCallback(
(credentials: FormData) => {
login(credentials);
},
[call]
);
const validate = useCallback(async () => {
return await service.validate();
}, [service]);
interface IRequestState
interface IStateOptions
Property | Type |
---|
distinct | <optional> boolean |
cache | <optional> string | boolean |
immediateFetch | <optional> boolean |
proxyMethod | "POST" | "GET" | "PUT" | "PATCH" | "DELETE" |
debug | <optional> boolean |
appendQuery | <optional> string |
enum FetchState
Field | Key |
---|
Fulfilled | 0 |
Idle | 1 |
Pending | 2 |
Rejected | 3 |
function IStateUpdater
Executes a fetch call and updates state properties accordingly. Returns true
if the http request was succesful, false
if not.
Arguments
body?
: Promise | Model | FormData | null
proxy?
: boolean
Returns
boolean
Examples
Shoe update example.
const { call } = usePatch(shoeService, '<id>');
const submitHandler = useCallback(
(formData: FormData) => {
call(formData);
},
[call]
);
const getData = useCallback(() => {
call(null, true);
}, [call]);