Socket
Socket
Sign inDemoInstall

zustand-fetching

Package Overview
Dependencies
11
Maintainers
1
Versions
47
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 1.1.4 to 2.0.1

dist/examples/controllers/1_Controller_Request.d.ts

2

dist/examples/common.d.ts
import { ReactNode } from "react";
import { ILoadingStatus } from "../interfaces/ContentLoading";
import { ILoadingStatus } from "../interfaces/IContentLoading";
interface ISwitcherProps {

@@ -4,0 +4,0 @@ status: ILoadingStatus;

@@ -0,9 +1,5 @@

export * from "./helpers/contextStore";
export * from "./helpers/controllers";
export * from "./helpers/resettableStore";
export * from "./helpers/zustandContextStore";
export * from "./helpers/zustandGroupSlice";
export * from "./helpers/zustandList";
export * from "./helpers/zustandModal";
export * from "./helpers/zustandNormalizedList";
export * from "./helpers/zustandPrimitive";
export * from "./helpers/zustandSlice";
export * from "./interfaces/ContentLoading";
export * from "./helpers/slices";
export * from "./interfaces/IContentLoading";

@@ -0,9 +1,5 @@

export * from "./helpers/contextStore";
export * from "./helpers/controllers";
export * from "./helpers/resettableStore";
export * from "./helpers/zustandContextStore";
export * from "./helpers/zustandGroupSlice";
export * from "./helpers/zustandList";
export * from "./helpers/zustandModal";
export * from "./helpers/zustandNormalizedList";
export * from "./helpers/zustandPrimitive";
export * from "./helpers/zustandSlice";
export * from "./interfaces/ContentLoading";
export * from "./helpers/slices";
export * from "./interfaces/IContentLoading";
{
"name": "zustand-fetching",
"version": "1.1.4",
"version": "2.0.1",
"private": false,

@@ -61,2 +61,3 @@ "description": "Zustand fetching helpers",

"@types/react": "^18.0.33",
"@types/lodash-es": "^4.17.7",
"@typescript-eslint/eslint-plugin": "^5.57.1",

@@ -78,7 +79,12 @@ "@typescript-eslint/parser": "^5.57.1",

"typescript": "^4.9.5",
"zustand": "^4.3.7"
"zustand": "^4.3.7",
"immer": "^10.0.1",
"lodash-es": "^4.17.21"
},
"peerDependencies": {
"react": ">=17",
"zustand": ">=4"
"zustand": ">=4",
"lodash-es": ">=4",
"nanoid": ">=4",
"immer": ">=10"
},

@@ -85,0 +91,0 @@ "engines": {

# Zustand Fetching Helpers
Here are some helper functions for working with **zustand**.
The creators of **zustand** did a great job and we really enjoy using this state manager.
> Introducing several functions that simplify working with **zustand** and clean up your store from unnecessary actions
> and states.
> What is this library for? We always have standard requests to the backend or boilerplate, so we offer several
> methods to simplify the work with **zustand**. All examples are made using _TypeScript_
The functions described below are _**well-typed**_ and allow working with _**nested**_ objects. Zustand suggests writing
custom [slices](https://github.com/pmndrs/zustand/blob/main/docs/guides/slices-pattern.md) and dividing the store into
several parts. Here are examples of our helpers
for [slices](https://github.com/Hecmatyar/zustand-fetching/tree/main/src/examples/slices). However, in
most cases, we need to divide the store into several parts because we add a lot of **unnecessary** data to the store and
visually **overload** it.
**The best way is to start with [this](https://github.com/Hecmatyar/zustand-fetching/tree/main/src/examples) examples
and then come back to read the documentation**
I propose several helpers that will take on a significant portion of the typical data work in your store. First, it is
easier to see [examples](https://github.com/Hecmatyar/zustand-fetching/tree/main/src/examples/controllers) to understand
what it is and how it can help.
Problem: All asynchronous requests are actually very similar, but we are constantly faced with our own
implementation from different developers for each request. This makes it difficult to understanding and easy to miss
something. We present you a way to remove the burden of request's infrastructure and leave only control over fetching.
- [leitenRequest](https://github.com/Hecmatyar/zustand-fetching/blob/main/src/examples/controllers/1_Controller_Request.tsx)
help you to handle request (any async function) and catch errors, return **hook** with params of request, and have
methods:
action, clear, abort, set
- [leitenGroupRequest](https://github.com/Hecmatyar/zustand-fetching/blob/main/src/examples/controllers/6_Controller_GroupRequest.tsx)
handle a lot of similar requests dynamically, return **hook** with 2 overloads and have methods: call, clear
- [leitenRecord](https://github.com/Hecmatyar/zustand-fetching/blob/main/src/examples/controllers/2_Controller_Record.tsx)
working with objects, have methods: set, patch, clear
- [leitenPrimitive](https://github.com/Hecmatyar/zustand-fetching/blob/main/src/examples/controllers/3_Controller_Primitive.tsx)
working with data like with primitive value, but it can be object, function or primitives. Have methods set and clear
- [leitenList](https://github.com/Hecmatyar/zustand-fetching/blob/main/src/examples/controllers/4_Controller_List.tsx)
working with array. Have methods set, clear, add, update, remove, toggle, filter. If array item is an object then
set **compare** function in the controller's
options (third parameter)
- [leitenNormalizedList](https://github.com/Hecmatyar/zustand-fetching/blob/main/src/examples/controllers/4_Controller_List.tsx)
the same as leitenList but working with normalized state
- [leitenModal](https://github.com/Hecmatyar/zustand-fetching/blob/main/src/examples/controllers/5_Controller_Modal.tsx)
help to work with modals, have built in modal manager (if you want to open modal in cascade). Return hooks
with [openState, hiddenState], have methods open, close and action
## Request
> All leitenControllers automatically calculate required type by path and **throw typescript error** if the specified
> path does not satisfy the requirements of the controller or the established types.
> Examples:
>- Argument of type '"info.keywords.1"' is not assignable to parameter of type '"info.keywords"'.
>- Argument of type 'string' is not assignable to parameter of type 'never'.
Here's what the simplest queries might look like.
## Advanced
```ts
export const useUser = create<IUserState>((set, get) => ({
...createSlice(set, get, "userRequest", async (id: string) => {
return getUserById(id);
}),
}))
```
### Options
When you read the _zustand_ documentation, you see that it recommends using [
slice-pattern](https://github.com/pmndrs/zustand/blob/main/docs/typescript.md#slices-pattern).
We have made a special helper for these purposes.
**leitenRecord**, **leitenPrimitive**, **leitenList** and **leitenNormalized** list have options with callbacks:
_processingBeforeSet_, _sideEffect_, _patchEffect_. You can use them to extend basic functionality
Let's imagine that you have a request that you want to execute. **_createSlice_** is a special method that will
automatically create all the necessary environment for working according to our description. For example, a request for
information about a user. Describe our store.
### Request
```ts
interface IUserState {
userRequest: ICreateRequest<string, IUser>
}
```
All requests working with **useLeitenRequests**. Usually you will never need it, but if you need it, then the record is
stored there with all the query parameters. The request key is returned by each leitenRequest
_**ICreateRequest**_ - interface that shows other developers and you that here used a helper to create a request.<br>
```ts
export type ICreateRequest<Payload, Result> = {
action: (params: Payload) => void;
atom: ContentLoading<Result, Payload>;
clear: () => void;
abort: () => void;
setAtom: (value: Partial<Result>, rewrite?: boolean) => void;
};
```
- _**action**_ - function to call our request.<br>
- _**atom**_ - request store. _ContentLoading_ indicates that this is loading data<br>
- _**clear**_ - function to clear the _atom_ field.<br>
- _**abort**_ - function to abort the request. Useful in case we leave the page where the request was called before the
end of the request.<br>
- _**setAtom**_ - set content field in our _atom_. You can use _setAtom_ in the same way like _zustand_ _set_.
The _atom_ field from **_ICreateRequest_** is the _**ContentLoading**_ interface.
```ts
export interface ContentLoading<Content, Payload = undefined> {
content: Content | null;
status: ILoadingStatus;
payload?: Payload | null;
error?: any;
lastFetchTime: Date | null;
}
```
> Thanks to _ContentLoading_, we do not need to declare the request status separately, create a field to store the
> request execution error, etc. We can always get the content of the request and display it inside the component,
> show the process or the error.
- _**content**_ - the data returned by request. _null_ - when we haven't received anything yet<br>
- _**status**_ - the status of our request. Possible values: "init", "loading", "loaded", "waiting", "progress", "
error"<br>
- _**payload**_ - our request's payload<br>
- _**error**_ - the error returned by the request<br>
- _**lastFetchTime**_ - Date of last fulfilled request<br>
Congratulations, we have reviewed the interface of our _**createSlice**_.
Let's create a simple store with request to get information about the user.
**Please note** that the name passed to _**createSlice**_ must match what you defined in _IUserState_.
```ts
export const useUser = create<IUserState>((set, get) => ({
...createSlice(set, get, "userRequest", async (id: string) => {
return getUserById(id);
}),
}))
```
Thus, we created a request for get user data. **getUserById** is your data request, which should return the type _IUser_
.
**That's all.** 3 lines to describe our request. What can we do with it now? let's see. We used a small _StatusSwitcher_
component to keep the example component code cleaner.
```tsx
export const User = ({ id }: { id: string }) => {
const { atom, action } = useUser((state) => state.userRequest);
useEffect(() => {
action(id); // call request using id param
}, [action, id])
return (
<div>
<StatusSwitcher status={atom.status} error={atom.error.message}>
User name: <b>{atom.content?.name}</b> // we will see it when loading will be done
</StatusSwitcher>
</div>
);
};
interface ISwitcherProps {
status: ILoadingStatus;
children: ReactNode;
error: string
interface IState {
user: IUser | null;
}
const StatusSwitcher = ({ status, children, error }: ISwitcherProps) => {
return <>
{status === "loaded" && <>{children}</>}
{status === "loading" && <>loading...</>}
{status === "error" && <>{error.result.message}</>}
</>
}
```
const useExampleStore = create<IState>(() => ({
user: null,
}));
What we got:
const useController = leitenRequest(useExampleStore, "user", getUser);
- we always know the status of the request<br>
- we can get request data or its execution error<br>
- we have a simple way to call a request<br>
- we needed a minimum of type descriptions and fields in our store<br>
You also can add any data processing to your request, use your own _baseFetch_ handlers or some solutions. The main
thing is that the returned result must match the type you declared in ```userRequest: ICreateRequest<string, IUser>```
. For example, let's process the result of a query
```ts
export const useUser = create<IUserState>((set, get) => ({
...createSlice(set, get, "userRequest", async (id: string) => {
const result = await getUserById(id);
return { ...result.data, role: "artist" }
}),
}))
```
But that's not all, _**createSlice**_ has much more powerful functionality. Here is an advanced description of the
parameters of ```createSlice(set, get, name, payloadCreator, extra)```
- _**set and get**_ are methods from our _zustand_ store <br>
- _**name**_ - the name of our request. **Please note** that it must match the one you defined in the type
your store<br>
- _**payloadCreator**_ is the function inside which we execute our request. Important _payloadCreator_ has an object
with field ```signal: AbortSignal``` as the second argument. It can be passed to your _**fetch**_ request and
calling the ```userRequest.abort``` method will cancel the request.<br>
Thus, you can edit the previous example using _abort_
```tsx
const { action, abort } = useUser((state) => state.userRequest);
useEffect(() => {
action("id"); // call request using id param
return () => {
abort() // abort request when we anmout our compoenent
}
}, [action])
```
Then we pass _signal_ to our request. More about how to use the _signal_
in [fetch](https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal)
```ts
export const useUser = create<IUserState>((set, get) => ({
...createSlice(set, get, "userRequest", async (id: string, { signal }) => {
return getUserById(id, signal);
}),
}))
```
- **_extra_** is an object that allows you to take full control over the execution of the request and do magic.
All fields can be conditionally divided into 3 groups: fields of an atom, reactions and a reducer.
- _**initialStatus**_ - the status that can be defined for our request by default. Default value is **"
loading"**, but you can define some of LoadingStatus. Indeed, a lot depends on the status in our interface and we do
not always need **loading**.
- _**initialContent**_ - content of the _atom_ field. The default is defined as _null_ until a value is returned from
the request.
Update our userRequest and use these fields
```ts
export const useUser = create<IUserState>((set, get) => ({
...createSlice(set, get, "userRequest", async (id: string, { signal }) => {
return getUserById(id, signal);
}, { initialStatus: "init", initialContent: { name: "John Doe" } }),
}))
```
The following fields are reactions that are called for your request lifecycle. Very useful for alerts.
- **_fulfilledReaction_** - called when request was successful <br>
- **_rejectedReaction_** - called when request was rejected <br>
- **_resolvedReaction_** - called after the request is executed, regardless of the result <br>
- **_actionReaction_** - called before the start of the request <br>
- **_abortReaction_** - called when request was aborted <br>
```ts
export const useUser = create<IUserState>((set, get) => ({
...createSlice(set, get, "userRequest", async (id: string, { signal }) => {
return getUserById(id, signal);
}, {
fulfilledReaction: (result: IUser, id: string) => {
scheduleRequest.action(id) //example, call request to get schedule for user
},
rejectedReaction: () => {
notification.error("Some error was accured")
},
actionReaction: (id: string) => {
log("Log user", id)
}
}),
}))
```
And the last field is _contentReducers_.
- _**contentReducers**_ - with it we can fully manage the data we put in _content_.
There are 4 fields in total _pending_, _fulfilled_, _rejected_, _aborted_. Each of these functions will be called at
its own stage of the request execution.
What is it for? For example, we want to replace query data. This is very useful because we don't need to write a lot of
logic in the slice request body.
```ts
export const useUser = create<IUserState>((set, get) => ({
...createSlice(set, get, "userRequest", async (id: string, { signal }) => {
return getUserById(id, signal);
}, {
contentReducers: {
pending: () => ({}) //todo work in proggress
},
}),
}))
```
We previously made a request to get a _schedule_ for a _user_ as an example. Let's update the _IUserState_ that will
execute the request to get information about the user and his schedule.
```ts
interface IUserState {
userRequest: ICreateRequest<string, IUser>
scheduleRequest: ICreateRequest<string, ISchedule>
const User = () => {
const status = useLeitenRequests(state => state[useController.key].status)
return <>{status}</>
}
export const useUser = create<IUserState>((set, get) => ({
...createSlice(set, get, "userRequest", async (id: string) => {
return getUserById(id);
}),
...createSlice(set, get, "scheduleRequest", async (id: string) => {
return getScheduleById(id);
}),
}))
```
We have described one more request. The user's schedule will be called if we get the user's data. But you can use
and call _request.action_ in any order you want.
leitenMap also can be
helpful, [example](https://github.com/Hecmatyar/zustand-fetching/blob/main/src/examples/controllers/6_Controller_GroupRequest.tsx)
## GroupRequest
### Group Request
Sometimes we have a situation where we need to execute a series of single requests asynchronously. For example, we have
a list of users, and we want to request the schedule of each of them only in the case when the user clicks on the
button. To do this, the helper for executing group queries _**createGroupSlice**_ will help us.
leitenGroupRequest return overloaded hook
Update the store in accordance with the new requirements
```ts
// created slice only for schedules
interface IScheduleSlice {
schedulesRequest: ICreateGroupRequests<string, ISchedule>;
}
export const scheduleSlice = <T extends IScheduleSlice>(
set: SetState<T>,
get: GetState<T>,
): IUserSlice => ({
...createGroupSlice(set, get, "schedulesRequest", async ({ payload, key }: IGroupRequestParams<string>) => {
return getScheduleById(payload);
}),
});
```
Then our user component will look like this
```tsx
export const User = ({ id }: { id: string }) => {
const { atom, action } = useCommon((state) => state.userRequest);
const call = useCommon((state) => state.schedulesRequest.call);
const [isOpen, setIsOpen] = useState(false)
useEffect(() => {
action(id);
}, [action, id])
return (
<div>
User name: <b>{atom.content?.name}</b>
<button onClick={() => setIsOpen(true)}>get schedule</button>
// open user's schedule
{isOpen ? <Schedule id={id} /> : <></>}
</div>
);
};
const Schedule = ({ id }: { id: string }) => {
const { status, content, error } = useCommon((state) => state.schedulesRequest.get(id), shallow);
useEffect(() => {
call([{ key: id, payload: id }]) // call request for user's schedule
}, [action, id])
return <StatusSwitcher status={status} error={error}> //use StatusSwitcher component again
Shedule: {content.days} //for example, show some scheduled days
</StatusSwitcher>
interface IState {
cards: Record<string, ICard>;
}
```
After the request is fulfilled, the schedule continues to be stored. For example, can be reused without the need to
execute the
request again.
const useExampleStore = create<IState>(() => ({
cards: {},
}));
export const useGroupController = leitenGroupRequest(
useExampleStore,
"cards",
async (props: ILeitenGroupRequestParams<string>) => {
return getCard(props.params);
},
);
The list of fields that _**createGroupSlice**_ accepts is identical to _**createSlice**_. The difference is what
creates _**createGroupSlice**_
- _**requests**_ - an object that contains all our requests. The queries are identical to those returned by _**
createSlice**_. Usually we don't need to interact with it. The keys in the object are the _key_ that you passed to
the _call_ function. Be careful that these values should be unique!
- _**call**_ - a function that accepts an array of objects of the _IGroupRequestParams_ type. This means that requests
will be called with the _payload_ that we passed in and a unique key.
```ts
export interface IGroupRequestParams<Payload> {
key: string; // necessarily to store request
payload: Payload;
}
```
- _**get**_ - function that has an optional _key_ parameter. It will return the request by _key_ or all requests if _
key_ is undefined.
- _**getContent**_ - will return the content of the selected request by _key_. It is necessary in a situation when we
work only with request data.
- _**clear**_ - has an optional _key_ parameter. Clears whole store or only the selected query by key.
-
Now we have the ability to create a dynamic number of requests of the same type without the need to describe them in
your store.
## Modal window
_**createModal**_ - a helper for creating everything you need to work with a modal window. It happens often that we need
to call a modal window from different components and pass data to it (for example, open a modal window to delete entity
by id).
```ts
interface IUserState {
userListRequest: ICreateRequest<void, IUser[]>
removeModal: IModalCreator<{ id: string }>;
}
export const useUser = create<IUserState>((set, get) => ({
...createModal(set, get, "removeModal", { id: "" }),
...createSlice(set, get, "userListRequest", async () => {
return getUserList();
}),
}))
```
The first three arguments to createModal are the same as those of _**createSlice**_, and the last is the modal's storage
value. If it is not needed, then you can simply write _undefined_.
```tsx
const Page = () => {
const { content } = useUser((state) => state.userListRequest.content || [])
return <>
{content.map(user => <User key={user.id} user={user} />)}
<RemoveModal /> // we can put our modal in the root of page
</>
}
export const User = ({ user }: { user: IUser }) => {
const { open } = useUser((state) => state.removeModal);
return (
<div>
User name: {user.name}
<button onClick={() => open({ id: user.id })}>remove</button>
</div>
);
};
export const Modal = () => {
const { atom, close } = useUser((state) => state.removeModal);
const action = useUser((state) => state.removeRequest.action); //сделаем вид что у нас есть запрос на удаление пользователя
const handleRemove = usecallback(() => {
action(atom.data.id)
}, [])
return (
<Modal isOpen={atom.isOpen}>
<button onClick={close}>cancel</button>
<button onClick={handleRemove}>confirm</button>
</Modal>
);
};
```
Now we don't have to worry about forwarding the necessary props or declaring the state of the modal somewhere.
//todo work in progress
const status = useGroupController(id, (state) => state.status); //First param is key, better option
or
const requests = useGroupController((state) => state); // Record with all requests
```
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc