Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to extract common logic and reuse it in multiple store #161

Closed
csr632 opened this issue Aug 20, 2020 · 24 comments
Closed

How to extract common logic and reuse it in multiple store #161

csr632 opened this issue Aug 20, 2020 · 24 comments

Comments

@csr632
Copy link

csr632 commented Aug 20, 2020

How to extract common logic from store and reuse it in multiple store (or for multiple times)?

For example, we often need to track the loading state and error state of requsts. How to reuse this logic?

Here is some suggestion from redux community. In the react hook world, we have swr.

If zustand have solution for this, we should put it in the documentation!

@csr632 csr632 changed the title How to extract common logic from store and reuse it in multiple store How to extract common logic and reuse it in multiple store Aug 20, 2020
@dai-shi
Copy link
Member

dai-shi commented Aug 20, 2020

store logic in zustand is just a function. So, you could define a common function. There's also middleware. Do you have any suggestion?

Is the example something like this?

const useStore = create(set => {
  result: null,
  loading: false,
  error: null,
  fetch: async url => {
    try {
      set({ loading: true })
      const result = await (await fetch(url)).json()
      set({ loading: false, result })
    } catch(error) {
      set({ loading: false, error })
    }
  },
});

@csr632
Copy link
Author

csr632 commented Aug 21, 2020

In a real-wrold App, there will be many stores manage their loading state:

// This is the store for Page1
const usePageModel1 = create(set => {
  result: null,
  loading: false,
  error: null,
  fetchData1: async (id)=> {
    try {
      set({ loading: true })
      const result = await (await fetch(`/api1/${id}`)).json()  // load data from api1
      set({ loading: false, result })
    } catch(error) {
      set({ loading: false, error })
    }
  },
});

const Page1 = () => {
   const model = usePageModel1();
   // ...
}

// This is the store for Page2
const usePageModel2 = create(set => {
  result: null,
  loading: false,
  error: null,
  fetchData2: async (id)=> {
    try {
      set({ loading: true })
      const result = await (await fetch(`/api2/${id}`)).json()  // load data from api2
      set({ loading: false, result })
    } catch(error) {
      set({ loading: false, error })
    }
  },
});

const Page2 = () => {
   const model = usePageModel2();
   // ...
}

There is clearly some duplicate code between these two models. So I wonder how to extract this logic and reuse it. @dai-shi

@dai-shi
Copy link
Member

dai-shi commented Aug 21, 2020

How about this?

const createFetchStore = (fetchActionName, baseUrl) => create(set => {
  result: null,
  loading: false,
  error: null,
  [fetchActionName]: async (id)=> {
    try {
      set({ loading: true })
      const result = await (await fetch(`${baseUrl}/${id}`)).json()
      set({ loading: false, result })
    } catch(error) {
      set({ loading: false, error })
    }
  },
});

@csr632
Copy link
Author

csr632 commented Aug 22, 2020

Cool. So the key to code reuse is store factory function:

// factory
const createFetchStore = (baseUrl) => create(set => {
  result: null,
  loading: false,
  error: null,
  fetch: async (id)=> {
    try {
      set({ loading: true })
      const result = await (await fetch(`${baseUrl}/${id}`)).json()
      set({ loading: false, result })
    } catch(error) {
      set({ loading: false, error })
    }
  },
});

// store for Page1
const usePageStore1 = createFetchStore('/api1/');
const Page1 = () => {
   const model = usePageStore1();
   // ...
}

// store for Page2
const usePageStore2 = createFetchStore('/api2/');
const Page2 = () => {
   const model = usePageStore2();
   // ...
}

Cool. It is cleaner than the redux way.

But I am looking for a composable way to use zustand. In the world of react hooks, we can extract the fetching logic, use it to fetch from multiple API, an compose them together, into one hook:

import useSWR from 'swr'

function useCombinedData() {
  const { data: events, error, isValidating } = useSWR('/api/events')
  const { data: projects, error, isValidating } = useSWR('/api/projects')
  const { data: user, error, isValidating } = useSWR('/api/user', { refreshInterval: 0 }) // don't refresh
  // combine these data...
  return combinedData;
}

The most composable way to use zustand, seems to be "compose multiple useStore into one hook":

const useStore1 = create(set => {/* ... */});
const useStore2 = create(set => {/* ... */});

function useCombinedData() {
  const { data1, reducer1 } = useStore1();
  const { data2, reducer2 } = useStore2();
  // combine these data...
  return combinedData;
}

@dai-shi
Copy link
Member

dai-shi commented Aug 22, 2020

Did it work? Great.

Here's another one. (I didn't run it.)

const createFetchState = baseUrl => set => ({
  result: null,
  loading: false,
  error: null,
  fetch: async (id)=> {
    try {
      set({ loading: true })
      const result = await (await fetch(`${baseUrl}/${id}`)).json()
      set({ loading: false, result })
    } catch(error) {
      set({ loading: false, error })
    }
  },
});

// store for Page1
const usePageStore1 = create(set => ({
  otherState: 'foo',
  ...createFetchState('/api1')(set),
});
const Page1 = () => {
   const model = usePageStore1();
   // ...
}

@csr632
Copy link
Author

csr632 commented Aug 22, 2020

Did it work? Great.

Here's another one. (I didn't run it.)

const createFetchState = baseUrl => set => ({
  result: null,
  loading: false,
  error: null,
  fetch: async (id)=> {
    try {
      set({ loading: true })
      const result = await (await fetch(`${baseUrl}/${id}`)).json()
      set({ loading: false, result })
    } catch(error) {
      set({ loading: false, error })
    }
  },
});

// store for Page1
const usePageStore1 = create(set => ({
  otherState: 'foo',
  ...createFetchState('/api1')(set),
});
const Page1 = () => {
   const model = usePageStore1();
   // ...
}

So the other way to reuse store logic is call other creator inside a creator.
I tweak your example to test how composable it is. I have some trouble using createFetchState for multiple times in one store:

const createFetchState = baseUrl => set => ({
  result: null,
  loading: false,
  error: null,
  fetch: async (id)=> {
    try {
      set({ loading: true })
      const result = await (await fetch(`${baseUrl}/${id}`)).json()
      set({ loading: false, result })
    } catch(error) {
      set({ loading: false, error })
    }
  },
});

// store for Page1
const usePageStore = create(set => {
  const api1FetchState = createFetchState('/api1')(set)
  const api2FetchState = createFetchState('/api2')(set)

  // try to combine the fetch of these two store
  const fetch = async (id) => {
      await api1FetchState.fetch(id); // it will set the store implicitly :(
      await api2FetchState.fetch(id);  // and the field keys will conflict
  }
  // How to combine the result of these two store?
  // const result = [...api1FetchState.result, ...api2FetchState.result]  // it doesn't works
  
  // what should I return here?
});
const Page1 = () => {
   const model = usePageStore();
   // ...
}

@dai-shi

@dai-shi
Copy link
Member

dai-shi commented Aug 22, 2020

Ah, so you want to reuse the common logic in one store. We'd need some kind of namespace then.

const scopedSet = (ns, set) => (update) => {
  set(prev => ({
    [ns]: { ...prev[ns], typeof update === 'function' ? update(prev[ns]) : update },
  })
}

const usePageStore = create(set => ({
  data1: createFetchState('/api1')(scopedSet('data1', set)),
  data2: createFetchState('/api2')(scopedSet('data2', set)),
})

and, then combining fetch actions...

const usePageStore = create((set, get) => ({
  data1: createFetchState('/api1')(scopedSet('data1', set)),
  data2: createFetchState('/api2')(scopedSet('data2', set)),
  fetch: async (id) => {
    await get().data1.fetch(id)
    await get().data2.fetch(id)
  },
})

You can also define fetch like you did in the previous comment without get().

Just an idea. Not sure this is an optimal solution. There can be a better one.

@dai-shi
Copy link
Member

dai-shi commented Aug 25, 2020

FYI, we had had a slightly related discussion in #163.
I found scopedSet is pretty easy with immer.

@csr632
Copy link
Author

csr632 commented Aug 25, 2020

scopedSet + immer looks perfect to me!
I will try it with typescript, and see if type inference works perfectly too.


I got some typescript issue, zustand can't infer the State type from my creator. See #95 (comment)

@dai-shi
Copy link
Member

dai-shi commented Sep 2, 2020

@csr632 Any updates?

@csr632
Copy link
Author

csr632 commented Sep 2, 2020

@dai-shi
Current zustand API is not friendly to ts, see #95 (comment)

@dai-shi
Copy link
Member

dai-shi commented Sep 2, 2020

If it's about ts friendliness, maybe, we should open a new issue for discussion then, instead of using the old issue, and close this issue?

@dai-shi
Copy link
Member

dai-shi commented Sep 2, 2020

Or, maybe just close this and reopen the other one because it has good discussion there.

@DalderupMaurice
Copy link

DalderupMaurice commented Apr 21, 2021

How about this?

const createFetchStore = (fetchActionName, baseUrl) => create(set => {
  result: null,
  loading: false,
  error: null,
  [fetchActionName]: async (id)=> {
    try {
      set({ loading: true })
      const result = await (await fetch(`${baseUrl}/${id}`)).json()
      set({ loading: false, result })
    } catch(error) {
      set({ loading: false, error })
    }
  },
});

I'd like to add to this solution that it's not always an API call that requires you to keep track of the loading state.
What if you're using your own service or another NPM library that executes code asynchronously.
With this, you cannot use your proposed solution.

For ex:

const useWalletStore = create(set => {
  result: null,
  loading: false,
  error: null,
  fetch: async url => {
    try {
      set({ loading: true })
      const result = await MyWalletService.generateWallet();
      set({ loading: false, result })
    } catch(error) {
      set({ loading: false, error })
    }
  },
});

import ChartJS from "chart-js";

const useChartStore = create(set => {
  result: null,
  loading: false,
  error: null,
  fetch: async url => {
    try {
      set({ loading: true })
      const result = await MyWalletService.getBalance();
      const chartData = await ChartJS.generate(result); // this is dummy code, just as an example
      set({ loading: false, chartData })
    } catch(error) {
      set({ loading: false, error })
    }
  },
});

in this example, you see the usage of different services that each use async operations, but there is no way to remove the duplicated code of the loading state.

It'd be nice if there was some kind of middleware to automatically add the loading and error property and set the loading state before/after the action is done or if it fails, to set the error property and loading state as well.

@dai-shi
Copy link
Member

dai-shi commented Apr 21, 2021

But, then you could accept custom fetchFunc instead of baseUrl?

const createFetchStore = (fetchActionName, fetchFunc) => create(set => {
  result: null,
  loading: false,
  error: null,
  [fetchActionName]: async (id)=> {
    try {
      set({ loading: true })
      const result = await fetchFunc(id)
      set({ loading: false, result })
    } catch(error) {
      set({ loading: false, error })
    }
  },
});

You could extract the logic in another way. Someone can try it as middleware.

@DalderupMaurice
Copy link

But, then you could accept custom fetchFunc instead of baseUrl?

const createFetchStore = (fetchActionName, fetchFunc) => create(set => {
  result: null,
  loading: false,
  error: null,
  [fetchActionName]: async (id)=> {
    try {
      set({ loading: true })
      const result = await fetchFunc(id)
      set({ loading: false, result })
    } catch(error) {
      set({ loading: false, error })
    }
  },
});

You could extract the logic in another way. Someone can try it as middleware.

This would mean you'd have to pass the service name, fetchFunc, or whatever you're calling from your react component, but there are a couple of problems with that:

  • An action shouldn't be limited to a 1-off method call (which is the case here)
  • Ideally, the react component shouldn't know anything about whatever you're doing in the action (separation of concerns).
  • When you change the function name/service name or whatever, the action call breaks (if you're using strings to reference what method should be called)

A middleware would be the way to go IMO.
I could create one (I think) but I'm not comfortable with creating the typing of it.

@dai-shi
Copy link
Member

dai-shi commented Apr 21, 2021

not comfortable with creating the typing of it.

Yeah, middleware is hard for typing. I'd avoid it if possible. A store create helper would be easier.

Just in case, your example would like this with the helper:

const useWalletStore = createFetchStore('fetch', async (url) => {
  const result = await MyWalletService.generateWallet();
  return result;
});

import ChartJS from "chart-js";

const useChartStore = createFetchStore('fetch', async (url) => {
  const result = await MyWalletService.getBalance();
  const chartData = await ChartJS.generate(result); // this is dummy code, just as an example
  return chartData;
});

@DalderupMaurice
Copy link

DalderupMaurice commented Apr 25, 2021

not comfortable with creating the typing of it.

Yeah, middleware is hard for typing. I'd avoid it if possible. A store create helper would be easier.

Just in case, your example would like this with the helper:

const useWalletStore = createFetchStore('fetch', async (url) => {
  const result = await MyWalletService.generateWallet();
  return result;
});

import ChartJS from "chart-js";

const useChartStore = createFetchStore('fetch', async (url) => {
  const result = await MyWalletService.getBalance();
  const chartData = await ChartJS.generate(result); // this is dummy code, just as an example
  return chartData;
});

I get your approach and it's a good one, but I still miss a piece of the puzzle.
In your scenario, you create individual stores for each individual action which makes the "get" method quite useless if you'd like to access values from other stores.

For example (no actual code, just made something up):

const useSettingStore = create(set => {
  currency: "EURO",
  decimales: 2,
  hidden: false,
  setCurrency: newCurrency => set({ currency: newCurrency  }),
  .....
});

const useChartStore = createFetchStore('fetchChartData', async (decimals, currency, hidden, account, tokens, balances) => {
  if(hidden) return "****"; // <-- Cannot get this setting using get().hidden;
  return ChartService.getChart(decimals, currency, account, tokens, balances); // <-- Only way to get the params is by passing it down...
});

// .. other async stores made by createFetchStore (useTokenStore, useBalancesStore, useAccountsStore, ....)

// Cluttered React Component
const WalletComponent = () => {
   const { currency, decimals, hidden } = useSettingStore(); // Settings store
   const { tokens } = useTokensStore(); // async action store create through "createFetchStore"
   const { balances } = useBalancesStore(); // async action store create through "createFetchStore"
   const { account } = useAccountStore(); // async action store create through "createFetchStore"
   const { chartData, fetchChartData } = useChartStore(); // async action store create through "createFetchStore"
   // Any other "async" variable needed (created using createFetchStore) would add a line as they are individual stores

  fetchChartData(decimals, currency, hidden, account, tokens, balances);   
}

In the example above, you cannot use get().xxxx and instead have to bloat your react component with hooks to retrieve this piece of data and pass it down to the action call.

Of course, this isn't a big problem in a small application, but it does become a problem when you're at a point where you have to pass down 1-3 of these values (Currency, Language, Network, any kind of setting, account data, or other metadata...) to each action you're calling, which clutters the codes massively in basically ALL of the components where you need some kind of data.

I've also tried to create a middleware for this issue, but I figured out that middlewares are just hooks to perform before and/or after the get, set, and/or api params. I couldn't figure out how to add a variable to the store inside a middleware and run it before and after the execution of the action instead.

@dai-shi
Copy link
Member

dai-shi commented Apr 25, 2021

if you'd like to access values from other stores.

In general, I would recommend to create a single store unless multiple stores are totally isolated.

In a single store scenario, if we need to extract common logic, we might extract store creator functions partially.

const createFetcher = (prefix, fetchFunc) => (set, get) => ({
  [prefix + 'result']: null,
  [prefix + 'loading']: false,
  [prefix + 'error']: null,
  [prefix + 'fetch']: async (arg)=> {
    try {
      set({ [prefix + 'loading']: true })
      const result = await fetchFunc(arg, { set, get })
      set({ [prefix + 'loading']: false, [prefix + 'result']: result })
    } catch(error) {
      set({ [prefix + 'loading']: false, [prefix + 'error']: error })
    }
  },
});

const fooFetcher = ('foo', async (id, { set, get }) => { ... });
const barFetcher = ('bar', async (id, { set, get }) => { ... });

const useStore = create((set, get) => ({
  ...fooFetcher(set, get),
  ...barFetcher(set, get),
}))

(Note: I never actually tried this approach...)

@DalderupMaurice
Copy link

if you'd like to access values from other stores.

In general, I would recommend to create a single store unless multiple stores are totally isolated.

In a single store scenario, if we need to extract common logic, we might extract store creator functions partially.

const createFetcher = (prefix, fetchFunc) => (set, get) => ({
  [prefix + 'result']: null,
  [prefix + 'loading']: false,
  [prefix + 'error']: null,
  [prefix + 'fetch']: async (arg)=> {
    try {
      set({ [prefix + 'loading']: true })
      const result = await fetchFunc(arg, { set, get })
      set({ [prefix + 'loading']: false, [prefix + 'result']: result })
    } catch(error) {
      set({ [prefix + 'loading']: false, [prefix + 'error']: error })
    }
  },
});

const fooFetcher = ('foo', async (id, { set, get }) => { ... });
const barFetcher = ('bar', async (id, { set, get }) => { ... });

const useStore = create((set, get) => ({
  ...fooFetcher(set, get),
  ...barFetcher(set, get),
}))

(Note: I never actually tried this approach...)

Nice! That should get the trick done I guess.
Lastly, sorry to bother you with this, but I do lose the typing/autocomplete with this example on useStore.

I currently have this:

import create, { GetState, SetState, State } from "zustand";

// Not sure how to type fetchFunc ---------------------------V-------V
const createFetcher = (prefix: string, fetchFunc: (...args: any) => any) => (
  set: SetState<State>,
  get: GetState<State>
) => ({
  [`${prefix}result`]: null,
  [`${prefix}loading`]: false,
  [`${prefix}error`]: null,
  [`${prefix}fetch`]: async (arg: any) => { // <----------- not sure how to type this
    try {
      set({ [`${prefix}loading`]: true });
      const result = await fetchFunc(arg, { set, get });
      set({ [`${prefix}loading`]: false, [`${prefix}result`]: result });
    } catch (error) {
      set({ [`${prefix}loading`]: false, [`${prefix}error`]: error });
    }
  }
});

const fooFetcher = createFetcher("foo", async (id, { set, get }) => {
  console.log("Calling fooFetcher...");
  return { foo: "fetcher" }; // returned value should set the correct typing of this function's return type
});

export const useStore = create((set, get) => ({
  ...fooFetcher(set, get)
}));

// React Component
const MyComponent = () => {
  const store = useStore();
  store.xxxx // <------- no typing/autocomplete
}

I'm only familiar with basic usage, not with these tricky typings.
Maybe it could also be a nice example for the docs as well :)

@dai-shi
Copy link
Member

dai-shi commented Apr 25, 2021

Oh, yeah, typing would be hard. Let me try.

const createFetcher = <Prefix extends string, Arg, Result>(
  prefix: Prefix,
  fetchFunc: (
    arg: Arg,
    opts: { set: SetState<State>; get: GetState<State> }
  ) => Promise<Result>
) => (set: SetState<State>, get: GetState<State>) => {
  type PrefixResult = `${Prefix}result`;
  type PrefixLoading = `${Prefix}loading`;
  type PrefixError = `${Prefix}error`;
  type PrefixFetch = `${Prefix}fetch`;
  return {
    [`${prefix}result`]: null,
    [`${prefix}loading`]: false,
    [`${prefix}error`]: null,
    [`${prefix}fetch`]: async (arg: Arg) => {
      try {
        set({ [`${prefix}loading`]: true });
        const result = await fetchFunc(arg, { set, get });
        set({ [`${prefix}loading`]: false, [`${prefix}result`]: result });
      } catch (error) {
        set({ [`${prefix}loading`]: false, [`${prefix}error`]: error });
      }
    }
  } as Record<PrefixResult, Result | null> &
    Record<PrefixLoading, boolean> &
    Record<PrefixError, Error | null> &
    Record<PrefixFetch, (arg: Arg) => Promise<void>>;
};

const fooFetcher = createFetcher("foo", async (id: number, { set, get }) => {
  console.log("Calling fooFetcher...");
  return { foo: "fetcher" };
});

typescript playground

I may prefer flat set and get in fetchFunc, instead of opts object.

@DalderupMaurice
Copy link

Oh, yeah, typing would be hard. Let me try.

const createFetcher = <Prefix extends string, Arg, Result>(
  prefix: Prefix,
  fetchFunc: (
    arg: Arg,
    opts: { set: SetState<State>; get: GetState<State> }
  ) => Promise<Result>
) => (set: SetState<State>, get: GetState<State>) => {
  type PrefixResult = `${Prefix}result`;
  type PrefixLoading = `${Prefix}loading`;
  type PrefixError = `${Prefix}error`;
  type PrefixFetch = `${Prefix}fetch`;
  return {
    [`${prefix}result`]: null,
    [`${prefix}loading`]: false,
    [`${prefix}error`]: null,
    [`${prefix}fetch`]: async (arg: Arg) => {
      try {
        set({ [`${prefix}loading`]: true });
        const result = await fetchFunc(arg, { set, get });
        set({ [`${prefix}loading`]: false, [`${prefix}result`]: result });
      } catch (error) {
        set({ [`${prefix}loading`]: false, [`${prefix}error`]: error });
      }
    }
  } as Record<PrefixResult, Result | null> &
    Record<PrefixLoading, boolean> &
    Record<PrefixError, Error | null> &
    Record<PrefixFetch, (arg: Arg) => Promise<void>>;
};

const fooFetcher = createFetcher("foo", async (id: number, { set, get }) => {
  console.log("Calling fooFetcher...");
  return { foo: "fetcher" };
});

typescript playground

I may prefer flat set and get in fetchFunc, instead of opts object.

That works! If I use the fooFetcher directly 😄
The only downside is that inside the createFetcher you lose the typings (get and set use State and State is just a record of unknowns)
and when you use the useStore (which also uses the State) you also don't have any typing typings/autocompletion.

I'll see if I can figure out how it's done, thanks for all the help already!

const fooFetcher = createFetcher("foo", async (id: number, set, get) => {
  console.log("Calling fooFetcher...");
  set({ foo: "fetcher" }); // no type checking/autocomplete
  get().x; // no typing/autocomplete
  return { foo: "fetcher" };
});

// useStore loses all typings
export const useStore = create((set, get) => ({
  ...fooFetcher(set, get)
}));

@DalderupMaurice
Copy link

Oh, yeah, typing would be hard. Let me try.

typescript playground

I may prefer flat set and get in fetchFunc, instead of opts object.

Did some adjustments, got this far: Typescript Playground
I created a State object for the combined state and passed it through, although it's not fully working.

@leyiang
Copy link

leyiang commented Mar 14, 2024

Oh, yeah, typing would be hard. Let me try.

const createFetcher = <Prefix extends string, Arg, Result>(
  prefix: Prefix,
  fetchFunc: (
    arg: Arg,
    opts: { set: SetState<State>; get: GetState<State> }
  ) => Promise<Result>
) => (set: SetState<State>, get: GetState<State>) => {
  type PrefixResult = `${Prefix}result`;
  type PrefixLoading = `${Prefix}loading`;
  type PrefixError = `${Prefix}error`;
  type PrefixFetch = `${Prefix}fetch`;
  return {
    [`${prefix}result`]: null,
    [`${prefix}loading`]: false,
    [`${prefix}error`]: null,
    [`${prefix}fetch`]: async (arg: Arg) => {
      try {
        set({ [`${prefix}loading`]: true });
        const result = await fetchFunc(arg, { set, get });
        set({ [`${prefix}loading`]: false, [`${prefix}result`]: result });
      } catch (error) {
        set({ [`${prefix}loading`]: false, [`${prefix}error`]: error });
      }
    }
  } as Record<PrefixResult, Result | null> &
    Record<PrefixLoading, boolean> &
    Record<PrefixError, Error | null> &
    Record<PrefixFetch, (arg: Arg) => Promise<void>>;
};

const fooFetcher = createFetcher("foo", async (id: number, { set, get }) => {
  console.log("Calling fooFetcher...");
  return { foo: "fetcher" };
});

typescript playground

I may prefer flat set and get in fetchFunc, instead of opts object.

I have two stores in my project that share same logics. I want to reuse them and add little unique logic to each of them. Thanks to Dai-Shi's reply, I can do it like this:

interface ItemWithID {
    id: number;
}

export interface ListStoreState<T> {
    items: T[],
    appendItems: (items: T[]) => void;
    // other common logic
} 

export const createListStoreLogic = <T extends ItemWithID>() => (
    set: StoreApi<ListStoreState<T>>['setState'],
    get: StoreApi<ListStoreState<T>>["getState"]
) => ({
    appendItems: (items: T[]) => {
        set(state => {
            const newList = [ ...state.items, ...items ];
            return { items: newList }
        });
    },
})

// Actual store:
interface FolderStoreState extends ListStoreState<Folder> {
     // unique states and logics
}

export const useFolderStore = create<FolderStoreState>((set, get) => ({
    items: [],
    ...createListStoreLogic<Folder>()(set, get),
}));

Hope this could be useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants