Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

zustand-fetching

Package Overview
Dependencies
Maintainers
1
Versions
47
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

zustand-fetching

Zustand fetching helpers

  • 1.1.3
  • Source
  • npm
  • Socket score

Version published
Weekly downloads
4
decreased by-69.23%
Maintainers
1
Weekly downloads
 
Created
Source

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.

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. All examples are made using TypeScript

Please, look in folder examples!

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.

Request

Here's what the simplest queries might look like.

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 request's payload
  • 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 .

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.result.message}</>}
  </>
}

What we got:

  • we always know the status of the request
  • we can get request data or its execution error
  • we have a simple way to call a request
  • we needed a minimum of type descriptions and fields in our store

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

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
  • name - the name of our request. Please note that it must match the one you defined in the type your store
  • 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.

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.

  • fulfilledReaction - called when request was successful
  • rejectedReaction - called when request was rejected
  • resolvedReaction - called after the request is executed, regardless of the result
  • actionReaction - called before the start of the request
  • abortReaction - called when request was aborted
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.

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.

GroupRequest

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

  • 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.
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).


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

Keywords

FAQs

Package last updated on 07 Apr 2023

Did you know?

Socket

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.

Install

Related posts

SocketSocket SOC 2 Logo

Product

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

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc