How do I reuse a type from a module in useState in a different module?

reasonreact

#1

Let say I have defined a type for a username+password pair:

// Credentials.re
type t = { user: string, password: string }

Now I have a component that wants to use that type as an internal state:

[@react.component]
let make = () => {
  let (state, setState) = React.useState( 
     (): Credentials.t => { user: "", password: "" }
  );
  let changeUser = (user: string) => {
    let newState: Credentials.t = { user, password: state.password };
    setState(newState); 
  } 
  // html goes here
}

I’m getting an error message when calling setState, something has the wrong type.

If I define the type inside the component, I don’t need the type annotations and everything works “magically”.


#2

Hi, I made an example based on your question, see here.

In the example, I’ve demonstrated two ways to declare an instance of record with the type Credentials.t:

/* 1. */
React.useState(() => {Credentials.user: "", password: ""})

/* 2. */
setState(_prevState => Credentials.{user, password: state.password}); /* known as open module */

Both will work.


#3

Thank you! Your example did not work right away, but it put me on the right track!

My example might have been misleading because I used t, which might have a special meaning in Reason (I’m just starting with this new language an don’t know all the idioms yet). In my original file, I called t credentialsPair instead. So this is what I ended up with:

// Credentials.re
type credentialsPair = { user: string, password: string };
let getCredentialsPair = ( user: string, password: string ): credentialsPair = {user, password}
// Component.re
let credentials = React.useState(() => Credentials.getCredentialsPair("", ""));

let changeCredentials = (user: string, password: string) => {
    setState( _prevState => Credentials.getCredentialsPair(user, password) ); 
} 

I did not know the syntax of calling setState with a function instead of a record, this was the part of your example that helped me. The error message I got originally on setState was

This expression has type Credentials.credentialsPair = Credentials.credentialsPair but an expression was expected of type Credentials.credentialsPair => Credentials.credentialsPair

Is this a general rule or pattern in ReasonReact that if you use a record type with React.useState you have to call the callback with a function instead of the record value?

Side note: In other places of my application I’ve used the React.useReducer instead (which would also have been a better fit for my original example). In this particular case, I’m always switching out the whole record, so I thought React.useState would be more appropriate.


#4

It is fine with your type naming. it was about type inference for the record types, in your case, type credentialssPair is defined in Credentials(.re) module, from Component(.re) perspective, credentialssPair is in another scope, type checker is unable to infer the record type, so we would need to explicitly introduce Credentials module into current scope.

Yes, ReasonReact’s useState only accepts a callback function, this is different than useReducer, see another examples of useState Hook and useReducer Hook in Examples section from ReasonReact docs.

If you dig into ReasonReact bindings a little bit more, you’ll find this:

(click here to expend the code snippet)
/*
 * Yeah, we know this api isn't great. tl;dr: useReducer instead.
 * It's because useState can take functions or non-function values and treats
 * them differently. Lazy initializer + callback which returns state is the
 * only way to safely have any type of state and be able to update it correctly.
 */
[@bs.module "react"]
external useState:
  ([@bs.uncurry] (unit => 'state)) => ('state, ('state => 'state) => unit) =
  "useState";