Tilia is born... still early version, ideas welcome :-)

Hello everyone,

I just release @tilia/react (and @tilia/core).

It’s a little state management library (using JS Proxy) inspired by Overmindjs (Greetings to Christian).

The library is written in ReScript, has no external dependencies and works for both TypeScript and of course, ReScript.

I take this opportunity to thank everyone involved in ReScript for their great work with the language, the tooling, compilation, etc. It’s a lot of work to make my developper’s experience so simple and I’m very grateful that I could focus so much on my little project without thinking about anything else.

Usage example:

type counter = {mutable count: int, mutable timesten: int}

let c = Tilia.make({
  count: 4,
  timesten: 0,
})

Tilia.observe(c, c => {
  c.timesten = c.count * 10
})

@react.component
let make = () => {
  let c = Tilia.use(c)
  <div>
    <h1> {c.timesten->Int.toString->React.string} </h1>
    <Button onClick={_ => c.count = c.count + 1}>
      {React.string(`count is ${c.count->Int.toString}`)}
    </Button>
  </div>
}
6 Likes

Amazing work! it’s great to see people using ReScript to build libraries, once “library mode” lands, I think we could really make ReScript the best platform to create libraries targeting JS and TS.

I’d love to hear your feedback about your experience, how much manual work was needed, if you had to use a bundler, etc!

Thanks!

2 Likes

Hello @tsnobip,

Step 1

The first step is to build the library and then find a way to avoid relying on rescript for JS bindings. This means rewriting somb bindings by hand or use nullable instead of option as adviced by @fham here.

Step 2

Decide on an API. Somehow the conventions are a little different if the library is used in ReScript, where you can expose methods such as “use” or in TypeScript where you cannot (because it is part of React and the habit is not to use namespaces so much).

This step was a bit confusing and I decided to have some differences (Tilia.use in ReScript vs useTilia in TS).

Step 3

Decide on creating the TypeScript types (and/or bindings to the compiled JS) by hand or with @genType.

I created the types by hand because the API surface is really small and I have an index.js file that does the rewiring to the TiliaCore.mjs files.

Step 4

Bundle. I decided to use esbuild for this and output for both cjs and esm modules.

Step 5

Configure package.json and .npmignore.

I had to use main and module entries to export for cjs and esm (using exports would hide ReScript sources).

My .npmignore keeps dist files and .res and .resi files in src.

For those interested, don’t hesitate to copy some files from the repo. I would be very proud if my little project helps others.

Step 6

Discover crazy bugs from your early adopters and fix them with passion :grimacing:

PS: I didn’t know with which semver version to use for my alpha publication. If I could go back in time, I would start at “0.1.0”…

2 Likes

This is very cool.

But shouldn’t React props always be immutable?

Fixed in 1.2.2.

From my understanding, the part that should be immutable is the props object itself. Using a proxy with mutations as a prop value seems fine.

But something that should not be done is to mutate during render.

Mutating the proxied object in an event should be fine.

Codesandbox example: playground.

1 Like

Great project! Like valtio but with Rescript bindings. I gave it a try because I needed to have observable data in React that could be changed outside of React. With React Context, one has to write a Provider and if you want to mutate the data, you are forced to do it from React, so any global API has to be wrapped in React component with useEffect which adds so much complexity. This allowed me to establish a global, observable state that can be mutated from anywhere. Brilliant.

The only problem I encounter initially is with nested objects. In your example

type clouds = {
  mutable morning: string,
  mutable evening: string,
}
type state = {
  mutable flowers: string,
  mutable clouds: clouds,
}

// Create a tracked object or array:
let tree = Tilia.make({
  flowers: "are beautiful",
  clouds: { morning: "can be pink", evening: "can be orange" },
})

If you mutate the tree by changing the clouds immutably, and some other part of the app subscribes to the clouds like use(tree.clouds) then since you are doing an immutable update of the tree the subscriber would not see any change, as the tree.clouds are replaced and the old reference is unchanged, but no longer referenced from the tree. This leaves the subscriber with stale reference. The solution is either

  • always do mutable changes even on nested objects, which seems like an anti-pattern in Rescript,
  • always subscribe to the whole tree which will notify subscribers about any change anywhere, but block fine-grained reactivity via subscribing only to certain tree branches.

My use-case involved an array in the tree, like

let tree = {
  messages: []
}

and obviously, I did an immutable update of the array as I’m trained to in React. I curious what tells you your instinct, should I do a mutable updates of an array? Or can the project be patched in a way, that nested objects keeps reference of the parent tree and can detect replacements of itself :thinking:

1 Like

Hi Czabaj,

Thanks for your interest !!

You should always track the whole tree, like this:

let tree = Tilia.use(tree)

Or if you receive a prop inside a component:

let branch = Tilia.use(branch)

This is because you always only track the exact keys you get from objects.

For example, if you use do

let tree = Tilia.use(tree)
Js.log(tree.foo.bar)

You are observing key “foo” in tree and key “bar” in foo.

This means that if someone changes tree.foo.banana, you are not notified because you never read the exact changed key “banana”.

But if someone replaces tree.foo, you are notified.

The only place where it is safe to not watch the whole tree is in child components receiving the branch from the parent because the parent will re-render if the whole branch is replaced and you will not have stale state.

I hope this clarifies it a little :blush: