zustand-fetching
Advanced tools
Comparing version 1.1.4 to 2.0.1
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": { |
476
README.md
# 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 | ||
``` |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
87656
75
1807
6
21
98
1