React Hooks: useState with Arrays and Lists


#1

I’ll do my best to explain my end goal here. I’m taking a React Hooks tutorial and implementing it in ReasonReact. It’s gone well so far:

  • Axios for request
  • bs-json for decoding
  • React.useState for search field text.

My issue is with using React.useState with an array or list.

When I decode the JSON received, I end up with an array(book). My intial value for React.useState is array(book), just like my decoded books. But when I compile, the code that is calling setBooks has an error:

This expression has type array(book)
       but an expression was expected of type array(book) => array(book)

I’m stuck and I’m still new to a lot of ReasonML. I get it’s expecting an array(book), but I thought that’s what I was giving it.

Code below:

type volumeInfo = {
  title: string,
  publishedDate: string,
};

type book = {
  id: string,
  meta: volumeInfo,
};

type books = {
  items: array(book),
};

module Decode = {
  let volumeInfo = volumeInfo =>
    Json.Decode.{
      title: volumeInfo |> field("title", string),
      publishedDate: volumeInfo |> field("publishedDate", string),
    };
  let book = book =>
    Json.Decode.{
      id: book |> field("id", string),
      meta: book |> field("volumeInfo", volumeInfo),
    };
  let books = books =>
    Json.Decode.{
      items: books |> field("items", array(book)),
    };
};

[@react.component]
let make = () => {
  let (searchTerm, setSearchTerm) = React.useState(() => "");
  let (books, setBooks) = React.useState(() => {
    let bookItems : array(book) = [||];
    bookItems;
  });

  let fetchBooks = () =>
    Js.Promise.(
      Axios.get("https://www.googleapis.com/books/v1/volumes?q=" ++ searchTerm)
      |> then_(response => response##data)
      |> then_(json =>
          json |> Decode.books
               |> (decodedBooks => {
                 Js.log(decodedBooks.items[0].meta.title);
                 setBooks(decodedBooks.items); // is array(book), expecting array(book)
               })
               |> resolve
        )
      // |> then_(response => resolve(setBooks(response##data)))
      |> catch(error => resolve(Js.log(error)))
    );

  let onSearchTermChange = (event) => {
    setSearchTerm(ReactEvent.Form.target(event)##value);
  };

  let onSubmitSearchForm = (event) => {
    fetchBooks();
    ReactEvent.Mouse.preventDefault(event);
  };

  <section>
    <form action="?">
      <label>
        <span>{ReasonReact.string("Search for Books")}</span>
        <input
          placeholder="microservice, restful, etc"
          value=(searchTerm)
          onChange=(onSearchTermChange)
        />
        <button onClick=(onSubmitSearchForm)>
          {ReasonReact.string("Search")}
        </button>
      </label>
    </form>
  </section>
};

#2

When you use setBooks, it expects to be passed a function that takes the previous state, which is an array(books), that returns an array(books).

Unlike the ReactJS version the Reason React version of useState always needs to be passed a function. In the instance here you would do something like setBooks(_ => decodedBooks.items).


#3

Thanks again Matt!

Curious, I can do this:

let (books, setBooks) = React.useState(() => {
    [||];
  });

And not get a type error when I set the array to an array of books. Is this because an array doesn’t really care what’s in it? Is there a better way to write the above code?


#4

OCaml (and by proxy Reason) has really good type inference. Because you set the empty array to an array holding books later, it knows that the underlying type of books is an array(books). Setting the initial value to an empty array, it is still technically an array of books.


#5

As for a better way to write it, you could omit the curly braces altogether. Reason automatically returns the final line in a block, so if you have a block with one line, you can omit the block altogether.


#6

And in fact, if you set up your editor to use refmt, it’ll strip those braces away.