Zustand controllers
Helps to avoid using other state managers to execute requests and allows you to work efficiently with zustand.
The Zustand Fetching Helpers library provides a set of functions and controllers that facilitate working with Zustand, a
state management library. The functions described below are well-typed and allow working with nested
objects. While Zustand suggests writing
custom slices to divide the store into
several parts, this library aims to simplify common data-related tasks without the need for additional state management
solutions.
To get a better understanding of what this library offers and how it works, you can refer to
the live example on CodeSandbox
. In many cases, the provided controllers will help reduce the complexity of your store, eliminating the need to split
it into multiple parts.
Installation
⚠️library changed name. You can install the library using npm:
npm install leiten-zustand
Since "Zustand" translates to "state" in German, we decided to adhere to the same naming strategy and used the word "
leiten" (meaning "lead" and "manage") to denote our controllers.
Common view
const useStore = create<IState>(() => ({ ... }));
const useController = leiten[Controller](useStore, "dot.nested.path", [options]);
Small Example
Let's create some fake example: load some data and then change it.
Pure zustand
const useStore = create<IStore>((set, get) => ({
loadData: async (id: string) => {
try {
set({ loadingData: true })
const response = await getData(id);
set({ data: response })
} catch {
} finally {
set({ loadingData: false })
}
},
loadingData: false,
data: { user: null, cards: [] },
updateUser: (user: Partial<IUser>) => {
set({ data: { ...get().data, user: { ...get().data?.user, ...user } } })
},
removeCard: (cardId: string) => {
const cards = get().data.cards.filter(card => card.id !== cardId);
set({ data: { ...get().data, cards } })
}
}))
With leiten controllers
const useStore = create<IStore>(() => ({
data: { user: null, cards: [] },
}));
const useRequest = leitenRequest(useStore, "data", (id: string) => getData(id));
const userController = leitenRecord(useStore, "data.user");
const cardsController = leitenList(useStore, "data.cards", { compare: (a, b) => a.id == b.id });
Using "leiten" controllers empowers you to simplify your state management by removing redundant actions, eliminating
unnecessary states, and reducing the complexity of working with nested objects. By adopting "leiten" controllers you can
create a cleaner and more streamlined store structure, making it easier to manage and manipulate your application's
data.
All actions and states for your zustand
store. Examples
- leitenRequest
Helps handle requests (any async function) and catch errors. Returns a hook with request parameters and provides
methods such as action, clear, abort, and set.
- leitenGroupRequest
Handles multiple similar requests dynamically. Returns a hook with two overloads and provides methods such
as action and clear. Can work with arrays as well as with the normalized list.
- leitenRecord
Works with objects and provides methods such as set, patch and clear.
- leitenPrimitive
Works with data as if it were a primitive value, but it can be an object, function, or primitives. Provides methods
such as set and clear.
- leitenList
Works with arrays and provides methods such as set, clear, add, update, remove, toggle, and filter. If
the array item
is an object, a compare function needs to be set in the controller's options (third parameter).
- leitenNormalizedList
Same as leitenList but works with normalized state.
- leitenModal
Helps work with modals and provides a built-in modal manager for cascading modals. Returns hooks
with [openState, hiddenState] and provides methods such as open, close and action.
- leitenFilterRequest
Same as leitenRequest but provide createFilter and listen methods, which allows you to create an
unlimited number of filters for the request. The request will automatically start action when the filter's patch
method is called. Or in case listen, the request will be executed if the observed value changes.
- leitenGroupFilterRequest
Same as leitenGroupRequest but provide createFilter method, which allows you to
create an
unlimited number of filters for the request. Works like leitenFilterRequest.
All leitenControllers automatically infer the required types based on the specified path and will throw a TypeScript
error if the provided path does not match the controller's requirements or 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'.
⚠️ If you encounter an error when specifying the path to your field in the store, it is likely because you are
attempting to attach a controller to a field with an incompatible type. Please ensure that the field you are attaching
the controller to has a permitted type to resolve this issue.
Library well tree shaking and have dependencies from immer, lodash-es and nanoid
Advanced
Options
leitenRecord, leitenPrimitive, leitenList and leitenNormalizedList have options with callbacks:
sideEffect and patchEffect. You can use them to extend basic functionality
const useExampleStore = create<IState>(() => ({ user: null }));
const recordController = leitenRecord(useExampleStore, "user", {
sideEffect: (value: { prev: IUser; next: IUser }) => {
},
patchEffect: (value: VALUE) => {
},
});
leitenRequest and leitenGroupRequest have a useful reactions: fulfilled, rejected, abort, resolved
and action
const useExampleStore = create<IState>(() => ({ user: null }));
const recordController = leitenRequest(useExampleStore, "user", async (id: string) => getUser(id), {
fulfilled: ({ previousResult, result, payload }) => {
},
rejected: ({ previousResult, error, payload }) => {
},
abort: ({ previousResult, payload }) => {
},
resolved: ({ previousResult, payload }) => {
},
action: ({ previousResult, payload }) => {
},
optimisticUpdate: (payload) => {
},
initialStatus: ILoadingStatus
});
- leitenList - if you are using object then you also should specify compare function like in example
- leitenNormalizedList - in addition to the compare function, you also need to define the getKey function
Request
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
interface IState {
user: IUser | null;
}
const useExampleStore = create<IState>(() => ({
user: null,
}));
const useController = leitenRequest(useExampleStore, "user", getUser);
const User = () => {
const status = useLeitenRequests(state => state[useController.key].status)
return <>{status}</>
}
leitenMap also can be helpful,
example
Group Request
leitenGroupRequest works equally well with both a normalized list and a regular array. If you are using an array, make
sure to specify the getKey function, as shown in the example
below. Codesandbox link with
arrays
interface IStore {
record: Record<string, ICard>,
array: ICard[],
}
const useStore = create<IStore>(() => ({
record: {},
array: []
}));
const useRecordController = leitenGroupRequest(useStore, "record", (id: string) => getCard(id))
const useArrayController = leitenGroupRequest(useStore, "array", (id: string) => getCard(id), {
getKey: (value) => value.id
})
leitenGroupRequest return overloaded hook
interface IState {
cards: Record<string, ICard>;
}
const useExampleStore = create<IState>(() => ({
cards: {},
}));
export const useGroupController = leitenGroupRequest(
useExampleStore,
"cards",
async (props: ILeitenGroupRequestParams<string>) => {
return getCard(props.params);
},
);
const status = useGroupController(id, (state) => state.status);
or
const requests = useGroupController((state) => state);
Store
The library provides wrappers
for ContextStore
and ResettableStore.
These wrappers can be used to enhance your Zustand store with additional features.