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!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.
Check out https://www.npmjs.com/package/use-sync-external-store
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.The former implementation seems to be more stable so I'm returning to it for now
Ok
Do you want any help debugging?
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?An easy test for 2 would be to use search in a standard component that uses location without problem
Alright so I tried and search breaks the app regardless of where it called so (thank god) is not an integration issue
So my last hunch is memoization of strings, that is the only variable I recognize...
What lib are you using to memoize it? Ramda?
moize
moize.deep
what's R.identity?
here's what I would try https://gist.github.com/lilactown/a3f44f713e256f506256816a6e27beaa/revisions
R.identity is x => x Simply returns what it's given...
could it be that the "search" result returns a different ordering of the results each time, which breaks the memoization approach you're using?
I'm just guessing, I'd have to see the rest of the code to be sure, maybe run it
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.
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
datascript.q is referentially transparent
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
Ohhh yeah, you right, because i wrapped it with my own q i forgot you pass datascript.db(conn)
That would be cleaner, I'll try that...
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...
I'll test it later with your gist and update
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)
);
};@lilactown Thank you for your help! 😄
Hooray!
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!!
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 😁datascript.q returns a new reference every time it's called
You'll need to memoize it somehow
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.
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