[Zustand] Some questions about types, is my case a limitation of Rescript or a bad practice?

Hello, I discover Zustand recently, and I really like the API. Unfortunatly i can’t make it work due to Rescript typing rules. Here is some code in order to help you understand.

First the bindings:

module type StoreConfig = {
  type state
}

module MakeStore = (Config: StoreConfig) => {
  type set = (. Config.state => Config.state) => unit
  type selector<'a> = Config.state => 'a
  type useStore<'a> = selector<'a> => 'a

  @module("zustand")
  external create: (set => Config.state) => useStore<'a> = "create"
}

And some code to test, and see the problem:

module AppStore = {
  type state = {
    counter: int,
    increment: unit => unit,
    decrement: unit => unit,
  }
}

module SomeStore = Zustand.MakeStore(AppStore)

let useStore = SomeStore.create(set => {
  counter: 0,
  increment: _ => set(.state => {...state, counter: state.counter + 1}),
  decrement: _ => set(.state => {...state, counter: state.counter - 1}),
})

let someCounter = useStore(state => state.counter)
let decrement = useStore(state => state.decrement)

I you install zustand npm i zustand --save and you add those 2 files to your project, you’ll see that rescript doesn’t like the second useStore because the first return an int, and the second return a function.

Does anybody knows how to use the function useStore with different types ? (I did some bindings for redux toolkit, and since i’m using an external function to get some data from the store, that way, it works…)

We do something similar with ReactRedux:

type selector<'state, 'a>

@module("react-redux")
external useSelectorFn: ('state => 'a) => 'a = "useSelector"

@module("react-redux")
external useSelectorSel: selector<'state, 'a> => 'a = "useSelector"

@module("react-redux")
external useDispatch: (unit) => (. 'action) => unit = "useDispatch"

....

  let dispatchLoggedIn = ReactRedux.useDispatch()
  let dispatchReload = ReactRedux.useDispatch()

....

      dispatchLoggedIn(. Actions.setUserLoggedInStatus())
      dispatchReload(. Actions.setreloadLeftNav())

The thunk we have there for useDispatch may be a key for you

@mouton thank for your answer, but if you look closely, the problem is different for Zustand. I already did some bindings for Redux Toolkit, and they look like yours, but in my case, i need to retrieve the return value of the function useStore.

As i understand, if useStore was an external bindings, it would work, but since it is not, it doesn’t work.

I think this is the value restriction at work. When you apply a function to its argument, the compiler gives the result value a single concrete type, and that type can’t change from one call to the next. But if you show the compiler that you’re actually creating a new function, the new function can be called with its argument and can return values of different types from one call to the next. Fixed code

Thank for your answer, but with your solution, i create a new store everytime i use the useStore function. Which is not what I’m trying to do. I need to call only once the create function, and then i can use the resulting store by passing a selector in order to get some value / actions / etc.

It seems that i cannot use Zustand in Rescript due to the type system.

There’s another option. Since useStore needs a function which returns the same type each time, just wrap up the returned values in a variant type which gives them all the same type. Link

Admittedly not as convenient, but it should get the job done.

I saw your solution, and I don’t think that it would be scalable enough. Thx for the tries :wink:
I decided to go with rematch, since it seems impossible to produce good bindings for zustand in Rescript.
Maybe in the future, it will be possible :slightly_smiling_face:

I made it to work with a little %identity hack. Hope that helps. Playground

module Zustand = {
  module type StoreConfig = {
    type state
  }

  module MakeStore = (Config: StoreConfig) => {
    type set = (. Config.state => Config.state) => unit
    type selector<'a> = Config.state => 'a

    type store

    external unsafeStoreToAny: store => 'a = "%identity"

    let use = (store: store, selector: selector<'a>): 'a =>
      unsafeStoreToAny(store)(. selector)

    @module("zustand")
    external create: (set => Config.state) => store = "create"
  }
}

module AppStore = {
  type state = {
    counter: int,
    increment: unit => unit,
    decrement: unit => unit,
  }
}

module SomeStore = Zustand.MakeStore(AppStore)

let store = SomeStore.create(set => {
  counter: 0,
  increment: _ => set(.state => {...state, counter: state.counter + 1}),
  decrement: _ => set(.state => {...state, counter: state.counter - 1}),
})

let someCounter = store->SomeStore.use(state => state.counter)
let decrement = store->SomeStore.use(state => state.decrement)
2 Likes

Wow, thanks a lot :grinning_face_with_smiling_eyes: !!!

It work as expected, and i don’t see any problem to use the little hack since everything is perfectly typed.

Let’s code :smiley:

1 Like

Just curious but are you using this in a React world? Seems like React.useReducer is right there?

Yes, I’m using Zustand with React, and no, i don’t want to use React.useReducer ou React.useState, since i need a store for my app. And we should not use react state with context to manage app state. Because on every context change, all the component would re-render, so it’s not opti at all.

That’s why we use tools, like redux and co :smiley:

I also use clean architecture on all my front-end project, in order to switch the ui easily (i’m using the same application logic on my web app and my native app, with react native for example).
So when i switch ui, i only have to code the new components.

2 Likes