datascript

2022-08-04T18:31:18.974169Z

Hello everyone! 😃 I'm new here so I'm sorry I'm jumping right into selfish questions 😅 I'm advocating Clojure/Script, datascript and datomic at my company. The way I want to introduce datascript is by using it as our front-end DB. Today our front-end is mainly TS and React so I wonder if anyone knows any good way to integrate DS with react? Today I'm using this snippet I found:

export const useQ = (query, ...args) => {
  const queryArgs = [query, datascript.db(conn), ...args];
  const [state, updateState] = useState(() => datascript.q(...queryArgs));

  const id = uuid();

  useEffect(() => {
    datascript.listen(conn, id, (data) => {
      if (data.tx_data.length) {
        const updatedQueryArgs = [query, data.db_after, ...args];
        const updatedState = datascript.q(...updatedQueryArgs);

        if (!equal(state, updatedState)) {
          updateState(updatedState);
        }
      }
    });
    return () => {
      return datascript.unlisten(conn);
    };
  }, [conn, query, args, id, state]);

  return state;
};
It looked reasonable but for some reason our app exploded (the browser got stuck in a very powerful computer) after a few navigations (the first thing I tried to save into the db was the current Location). Does anyone see any obvious flaws? Or knows any better integrations with react? (I tried homebase BTW but I need the DS db to be accessible outside of the react tree) Sorry for the long first message 😅 And thanks in advance!

2022-08-05T12:27:25.981599Z

BTW I found the mistake in the snippet, no key was passed to unlisten. Serve me right, I guess, for using a snippet of the internet without examining it properly.😅 Still investigating use-sync-external-store though, I'll post updates for others with similar issues.

lilactown 2022-08-04T20:47:06.904649Z

Check out https://www.npmjs.com/package/use-sync-external-store

🙏🏼 1
2022-08-08T07:29:32.492399Z

This implementation still seems buggy:

Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.

2022-08-08T07:31:50.659959Z

The former implementation seems to be more stable so I'm returning to it for now

lilactown 2022-08-08T08:45:42.953039Z

Ok

lilactown 2022-08-08T08:45:51.327029Z

Do you want any help debugging?

2022-08-08T08:59:05.845059Z

well it seems that I don't fully understand the behavior of useSyncExternalStore . But it's curious that in my use case I have (for now) 2 kinds of queries from the db: 1. Location:

[:find (pull ?location [*]) .
  :where
  [?location "ident" "${locationIdent}"]]
2. Search:
[:find ?search .
  :where
  [?location "ident" "${locationIdent}"]
  [?location "search" ?search]]
Now notice that search is actually just a string that is a part of location. Location is used ubiquitously throughout the app, and nothing breaks. The infinite loop happens only when I use the search query inside a very non-standard component. If I use another hook to get search directly from react-router it works. If I use the fixed useQ snippet (without useSyncExternalStore) it also works. So my hunches currently are: 1. maybe the memoization acts differently for strings? 2. maybe useSyncExternalStore behaviour coupled with the non-standard component causes the issue?

2022-08-08T08:59:41.657389Z

An easy test for 2 would be to use search in a standard component that uses location without problem

2022-08-08T09:17:15.435319Z

Alright so I tried and search breaks the app regardless of where it called so (thank god) is not an integration issue

2022-08-08T09:18:24.244079Z

So my last hunch is memoization of strings, that is the only variable I recognize...

lilactown 2022-08-08T15:44:06.595299Z

What lib are you using to memoize it? Ramda?

2022-08-08T15:47:06.231899Z

moize

2022-08-08T15:47:16.756539Z

moize.deep

lilactown 2022-08-08T16:31:52.496789Z

what's R.identity?

lilactown 2022-08-08T16:35:40.494989Z

here's what I would try https://gist.github.com/lilactown/a3f44f713e256f506256816a6e27beaa/revisions

2022-08-08T16:36:46.813779Z

R.identity is x => x Simply returns what it's given...

lilactown 2022-08-08T16:40:29.130219Z

could it be that the "search" result returns a different ordering of the results each time, which breaks the memoization approach you're using?

lilactown 2022-08-08T16:40:55.483079Z

I'm just guessing, I'd have to see the rest of the code to be sure, maybe run it

2022-08-08T16:45:35.653069Z

I'm don't think that datascript.q should be memoized, as memoization works only for referentially transparent functions. If I'd memoized it, the same query would get exactly the same result even though the underlying db has changed.

2022-08-08T16:47:42.470019Z

I'll take a deeper look at what search returns each time, also I'll test the memoization function on those results and see if it works as I think

lilactown 2022-08-08T16:55:35.028179Z

datascript.q is referentially transparent

lilactown 2022-08-08T16:56:27.837129Z

you pass in the query and the datascript.db(conn) each time. if the value returned by datascript.db changes, or the query changes, it would re-run the q function

2022-08-08T16:58:55.604029Z

Ohhh yeah, you right, because i wrapped it with my own q i forgot you pass datascript.db(conn)

2022-08-08T16:59:27.648819Z

That would be cleaner, I'll try that...

2022-08-08T17:34:38.106749Z

I think I found the problem! 😁 I haven't tested it yet, but the max number of cached results default to 1. So it wasn't search that was the problem but the fact that there were 2 queries...

2022-08-08T17:34:55.985109Z

I'll test it later with your gist and update

2022-08-08T18:36:26.247959Z

WORKS! 🥳 This is the final draft:

const dataScriptSubscribe = (listener) => {
  const key = datascript.listen(conn, listener);
  return () => datascript.unlisten(conn, key);
};

const memoQ = moize(datascript.q, {
  isDeepEqual: true,
  maxSize: Number.POSITIVE_INFINITY,
});

const getQueryData =
  (query, ...args) =>
  () => {
    const queryArgs = [query, datascript.db(conn), ...args];
    return memoQ(...queryArgs);
  };

export const useQ = (query, ...args) => {
  return useSyncExternalStore(
    dataScriptSubscribe,
    getQueryData(query, ...args)
  );
};

2022-08-08T18:37:11.882939Z

@lilactown Thank you for your help! 😄

lilactown 2022-08-08T19:51:11.307829Z

Hooray!

lilactown 2022-08-08T19:52:15.366529Z

You might consider using an LRU cache instead of an infinite size to avoid memory consumption. Not sure if moize supports that. But I'm glad it's working!!

2022-08-05T11:52:15.942029Z

I've been at it for an hour or so to understand the api and came up with this:

const dataScriptSubscribe = (listener) => {
   const key = datascript.listen(conn, listener);
   return () => datascript.unlisten(conn, key);
 };
 
 const getQueryData =
   (query, ...args) =>
   () => {
     const queryArgs = [query, datascript.db(conn), ...args];
     return datascript.q(...queryArgs);
   };
 
 export const useQ = (query, ...args) => {
   return useSyncExternalStore(
     dataScriptSubscribe,
     getQueryData(query, ...args)
   );
 };
It seems that the snapshot (in my what returns from the query) should be immutable (for referential equality) but it is not the case and so react blows up into an infinite loop. I'll continue hacking at it, but if you got any pointers that would be awesome 😁

lilactown 2022-08-05T19:07:57.405379Z

datascript.q returns a new reference every time it's called

lilactown 2022-08-05T19:08:26.820109Z

You'll need to memoize it somehow

2022-08-09T15:11:56.973459Z

moize does LRU, I wasn't sure what size to give it 🤔. I'm guessing it should be around o(n) where n is the size of concurrent queries.

2022-08-06T22:02:15.510529Z

Thank you! it only took me a second:

const dataScriptSubscribe = (listener) => {
  const key = datascript.listen(conn, listener);
  return () => datascript.unlisten(conn, key);
};

const memoize = moize.deep(R.identity);

const getQueryData =
  (query, ...args) =>
  () => {
    const queryArgs = [query, datascript.db(conn), ...args];
    return memoize(datascript.q(...queryArgs));
  };

export const useQ = (query, ...args) => {
  return useSyncExternalStore(
    dataScriptSubscribe,
    getQueryData(query, ...args)
  );
};
Much cleaner and work like a charm 🙂 Hopefully it'll be useful for others