In React/Vue/Solid, I want to express things like this:
```jsx
function BlogPostDetailComponent(...) {
// `subscribe` or `useSnapshot` or whatever would be the hook that gives me a reactive post object
const post = subscribe(Posts.find(props.id));
function updateAuthorName(newName) {
// This should handle the join between posts and authors, optimistically update the UI
post.author.name = newName;
// This should attempt to persist any pending changes to browser storage, then
// sync to remote db, rolling back changes if there's a failure, and
// giving me an easy way to show an error toast if the update failed.
post.save();
}
return (
<>
...
</>
)
}
```
I don't want to think about joining up-front, and I want the ORM to give me an object-graph-like API, not a SQL-like API.
In ActiveRecord, I can fall back to SQL or build my ORM query with the join specified to avoid N+1s, but in most cases I can just act as if my whole object graph is in memory, which is the ideal DX.
> In React/Vue/Solid, I want to say express things like this:
Here's what the React/Vue code would look like:
```
function BlogPostDetailComponent(props) {
// `useQuery` is equivelant to the `subscribe` that you mentioned:
const { isLoading, data, error } = db.useQuery({posts: {author: {}, $: {where: { id: props.id }, } })
if (isLoading) return ...
if (error) return ..
function updateAuthorName(newName) {
// `db.transact` does what you mentioned:
// it attempts to persist any pending changes to browser storage, then
// sync to remote db, rolling back changes if there's a failure, and
// gives an easy way to show an error toast if the update failed. (it's awaitable)
db.transact(
tx.authors[author.id].update({name: newName})
)
}
return (
<>
...
</>
)
Maybe a dumb question, but why do I have to wrap in `db.transact` and `tx.*`? Why can't I just have a proxy object that handles that stuff under the hood?
Naively, it seems more verbose than necessary.
Also, I like that in Rails, there are ways to mutate just in memory, and then ways to push the change to DB. I can just assign, and then changes are only pushed when I call `save()`. Or if I want to do it all-in-one, I can use something like `.update(..)`.
In the browser context, having this separation feels most useful for input elements. For example, I might have a page where the user can update their username. I want to simply pass in a value for the input element (controlled input)
ex.
```jsx
<input
value={user.name}
...
/>
```
But I only want to push the changes to the db (save) when the user clicks the save button at the bottom of the page.
If any changes go straight to the db, then I have two choices:
1. Use an uncontrolled input element. This is inconvenient if I want to use something like Zod for form validation
2. Create a temporary state for the WIP changes, because in this case I don't want partial, unvalidated/unconfirmed changes written to either my local or remote db.
This is a great question. We are working on a more concise transaction API, and are still in the design phase.
Writing a `user.save()` could be a good idea, but it opens up a question about how to do transactions. For example, saving _both_ user and post together).
I could see a variant where we return proxied objects from `useQuery`.
We have an internal lib for data management that’s philosophically similar to linear too. I opted for having required transactions for developer safety.
Imagine that you support the model discussed above where it’s possible to update the local store optimistically without syncing back to the db. Now you’re one missing .save() away from having everything look like it’s working in the frontend when really nothing is persisting. It’s the sort of foot gun that you might regret supporting.
Our model is slightly different in that we require the .save() on objects to create the mutation for the sync. The primary reason is that we’re syncing back to real tables in Postgres and require referential integrity etc to be maintained.
Mutating an object outside of a transaction is a hard error. Doing the mutation in a transaction but failing to call save within the same transaction is a hard error too.
Mark (our team member) has advocated for a callback-based API that looks a lot like what you landed on. It has the advantage of removing an import too!
Question: how do you solve the 'draft' state issue that remolacha mentioned?
I haven’t seen a better solution than remolacha’s #2 (create separate temporary state for the form).
Forms just inherently can have partially-finished/invalid states, and it feels wrong to try and kraal model objects into carrying intermediary/invalid data for them (and in some cases won’t work at all, eg if a single form field is parsed into structured data in the model)
I’m not quite seeing what you mean. What you mind redoing the example above for my benefit?
We have controllers that all the users actions are funnelled through. The top level functions in there are wrapped in transactions so in practice it’s not something you manually wrangle.
author.save()
.then(() => {
const article = new Article(db);
article.name = "New name";
article.author = author;
return article.save(); // could be used to chain the next save
})
.then(() => {
console.log("Both author and article saved successfully.");
})
.catch((error) => {
console.error("rollback, no changes");
});
but I confess I might have said that without having the same understanding of the problem as you so might be nonsense. it just happen that I decided to implement transactions this way in a side project of mine.
Gotcha, thanks for the clarification. I’m not sure what that would buy me here.
I have a rule that I only async if it’s a requirement. In my case I can carry out all the steps in a single (simple) sync action. Our updates are optimistic so we update all the models immediately and mobx reflects that in the react components on the next frame.
The network request for the mutation is the only thing that’s running async. If that fails we crash the app immediately rather than trying to rollback bits in the frontend. I know that approach isn’t for everyone but it works well for us.
@stopachka, sorry for late reply. I've mostly provided my ideal API in the posts above. I think my answer to transactions and forgetting save is to offer a few options, as in ActiveRecord. From what I recall, Rails gives a few ways to make persistent changes:
1. Assign, then save. AFAIK, this is effectively transactional if you're saving a single object, since it's a single `UPDATE` statement in sql. If you assigned to a related object, you need to save that separately.
2. Use ActiveRecord functions like `post.update({title: "foo", content: "Lorem ipsum"})`. This assigns to the in-memory object and also kicks off a request to the DB. This is basically syntax sugar over assigning and then calling `save()`, but addresses the issue around devs forgetting to call `save()` after assigning. In Rails, this is used in 90% of cases.
3. I can also choose to wrap mutations in a transaction if I'm mutating multiple proxy objects, and I need them to succeed/fail as a group. This is rarely used, but sometimes necessary. For example, in Rails, I can write something along the lines of this:
This gives transactional semantics around anything happening inside of the `do` block. I think the syntax would look very similar in javascript, for example:
In ActiveRecord, I can do this:
```rb
post = Post.find_by(author: "John Smith")
post.author.email = "john@example.com"
post.save
```
In React/Vue/Solid, I want to express things like this:
```jsx
function BlogPostDetailComponent(...) {
}```
I don't want to think about joining up-front, and I want the ORM to give me an object-graph-like API, not a SQL-like API.
In ActiveRecord, I can fall back to SQL or build my ORM query with the join specified to avoid N+1s, but in most cases I can just act as if my whole object graph is in memory, which is the ideal DX.