Security News
Cloudflare Adds Security.txt Setup Wizard
Cloudflare has launched a setup wizard allowing users to easily create and manage a security.txt file for vulnerability disclosure on their websites.
zustand-fetching
Advanced tools
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.
What is this library for? We always have standard requests to the backend, so we offer several methods to simplify the work with requests using zustand
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.
Here's what the simplest queries might look like. All examples are made using TypeScript
export const useUser = create<IUserState>((set, get) => ({
...createSlice(set, get, "userRequest", async (id: string) => {
return getUserById(id);
}),
}))
When you read the zustand documentation, you see that it recommends using slice-pattern. We have made a special helper for these purposes.
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.
interface IUserState {
userRequest: ICreateRequest<string, IUser>
}
ICreateRequest - interface that shows other developers and you that here used a helper to create a request.
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.
atom - request store. ContentLoading indicates that this is loading data
clear - function to clear the atom field.
abort - function to abort the request. Useful in case we leave the page where the request was called before the
end of the request.
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.
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
status - the status of our request. Possible values: "init", "loading", "loaded", "waiting", "progress", "
error"
payload - our payload with which we called the request
error - the error returned by the request
lastFetchTime - Date of last fulfilled request
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.
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_. This also means that you 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
export const useUser = create<IUserState>((set, get) => ({
...createSlice(set, get, "userRequest", async (id: string) => {
const result = await getUserById(id);
return { ...result.data, role: "artist" }
}),
}))
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.
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
}
const StatusSwitcher = ({ status, children, error }: ISwitcherProps) => {
return <>
{status === "loaded" && <>{children}</>}
{status === "loading" && <>loading...</>}
{status === "error" && <>{error}</>}
</>
}
What we got:
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)
signal: AbortSignal
as the second argument. It can be passed to your fetch request and
calling the userRequest.abort
method will cancel the request.Thus, you can edit the previous example using abort
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
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
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.
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.
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.
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.
interface IUserState {
userRequest: ICreateRequest<string, IUser>
scheduleRequest: ICreateRequest<string, ISchedule>
}
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.
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.
Update the store in accordance with the new requirements
// 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
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>
}
After the request is fulfilled, the schedule continues to be stored. For example, can be reused without the need to execute the request again.
The list of fields that createGroupSlice accepts is identical to createSlice. The difference is what creates createGroupSlice
export interface IGroupRequestParams<Payload> {
key: string; // necessarily to store request
payload: Payload;
}
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.
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).
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.
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
FAQs
Zustand state manager controllers
The npm package zustand-fetching receives a total of 9 weekly downloads. As such, zustand-fetching popularity was classified as not popular.
We found that zustand-fetching demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Cloudflare has launched a setup wizard allowing users to easily create and manage a security.txt file for vulnerability disclosure on their websites.
Security News
The Socket Research team breaks down a malicious npm package targeting the legitimate DOMPurify library. It uses obfuscated code to hide that it is exfiltrating browser and crypto wallet data.
Security News
ENISA’s 2024 report highlights the EU’s top cybersecurity threats, including rising DDoS attacks, ransomware, supply chain vulnerabilities, and weaponized AI.