This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2022-08-04
Channels
- # announcements (5)
- # aws (11)
- # babashka (15)
- # beginners (101)
- # biff (14)
- # calva (45)
- # clj-kondo (18)
- # cljs-dev (5)
- # clojure (178)
- # clojure-austin (5)
- # clojure-europe (8)
- # clojure-france (1)
- # clojure-nl (12)
- # clojure-norway (6)
- # clojure-spec (4)
- # clojure-uk (1)
- # clojurescript (13)
- # community-development (2)
- # conjure (6)
- # cursive (8)
- # datahike (1)
- # datalevin (3)
- # datascript (36)
- # datomic (6)
- # emacs (2)
- # etaoin (2)
- # fulcro (5)
- # graalvm (6)
- # gratitude (3)
- # introduce-yourself (1)
- # jobs-discuss (1)
- # lsp (19)
- # malli (4)
- # nbb (11)
- # off-topic (4)
- # other-languages (1)
- # pathom (19)
- # pedestal (1)
- # shadow-cljs (22)
- # spacemacs (16)
- # tools-deps (31)
- # vim (7)
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.
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 😁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 othersThis 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
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...
moize
moize.deep
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 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
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)
);
};
@U4YGF4NGM Thank you for your help! 😄
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!!
moize does LRU, I wasn't sure what size to give it :thinking_face:. I'm guessing it should be around o(n) where n is the size of concurrent queries.