jotai
jotai copied to clipboard
Improve debugging info in useAtomsDevtools
The current useAtomsDevtools is a great start for debugging tools. I've been playing with it a bit and want to suggest a few features:
-
Better atom labeling. Currently, atoms are labeled sequentially as
atom1,atom2, etc. Here's some ideas:- Automatically label atoms by capturing a stack trace during atom construction (in dev mode only), and adding a partial formatted stack trace to
atom.toString()'s output. I've experimented a little with this, for example an atom I create asconst testAtom = implicitAtom(...)has thistoString:WritableImplicitAtom 5 (created at Object.<anonymous> (/Users/jitl/src/jitl-monorepo/packages/state/src/lib/implicitAtom.test.ts:108:32)) - A less general option is to encourage setting
debugNameortoStringby adding a new optional final argument toatom()asatom(read, write, options?: { debugName?: string, toString?: () => string }); this would allow a nicer way to write atoms with labels in one line -atom(initialValue, { debugName: 'foo atom' })is easier to remember thanatom(initialValue); atom.debugName = 'foo atom'.
- Automatically label atoms by capturing a stack trace during atom construction (in dev mode only), and adding a partial formatted stack trace to
-
Better action/mutation/change labeling. Currently, the only info about a change is when it occurred. Ideally, we could expose the following information about each new snapshot on the fake Redux action:
- The place where the
setAtomStatefunction came from, eg the stack trace of theuseAtomoruseSetAtomhook that produced it. This allows the user to easily locate the React component that's updating Jotai's state. - The writable atoms (and/or their atom.toString()) involved in a state change. This could be complicated to gather for a derived writable atom that "fans out" by writing many other atoms, but we could at least capture the debug info of the top-level writable atom that started the write. That atom's identity is very analogous to the concept of "action type" in Redux.
- The place where the
-
Advanced: show why a change caused what React components to re-render. I call this “causality debugging”. This helps developers detect O(n) re-rendering due to change fan-out, which is a performance problem that can be very difficult to debug in large apps. This is the most complex idea, but I know it’s possible since I implemented this in my own framework. This would build on the stack trace capture work in 1 & 2. Here’s how it works:
- Whenever derived atom A reads a dependency atom D, save the stack trace of that
get(D)call alongside the revision number in the dependency map (or in a devtools only location). - Whenever a writer atom W function writes an atom WD, save the stack trace of the
set(WD, …)into a Map<W, [WD, Stack]> representing the action, stored in the devtools action log. - Whenever a component C subscribed to an atom via useAtom, capture a stack trace representing the component’s subscription.
- Whenever C re-renders due to an atom change, you can use the saved information described above to “join” together a causality event E describing “C rendered because it read A which was changed by W”. Associate E with the relevant action in the devtools action log
The way I expose this information to developers is by running a profile for N seconds, and then printing out a table of events E sorted by how many times the event occurred. So if a single atom changed and caused 100 components to re-render, that would cause 100 events and probably be at the top row of the table. But we could also might expose this info in the Redux devtools as part of the action log?
- Whenever derived atom A reads a dependency atom D, save the stack trace of that
Better atom labeling
We'd like to utilize atom.debugLabel. This is not only for useAtomsDevtools, but also for useAtomDevtools and Provider's React DevTools.
Our current solution is https://jotai.org/docs/api/babel#plugin-debug-label, but it only works for babel. The problem is there are many non-babel setups.
capturing a stack trace during atom construction (in dev mode only)
Sounds interesting. @Thisen What do you think?
Better action/mutation/change labeling
Is this possible? @Aslemammad Can you look into it and discuss about it?
@justjake are you also willing to work on those improvements?
@dai-shi I might have some spare time to pick away at some of these issues slowly - in some ways, I'm trying to share ideas inspired by the closed-source state management system used by my employer.
For a direction to follow around 1a using stack frames for labeling, we might be able to learn something from React's use of stack frame capturing here: https://github.com/facebook/react/blob/cae635054e17a6f107a39d328649137b83f25972/packages/react-devtools-shared/src/backend/DevToolsComponentStackFrame.js
I added a third idea which builds on top of the first two. Instrumenting my framework with all 3 ideas as suggested above took me 3-4 days of full-time work, but, I know my framework quite well and it’s simpler than Jotai because (1) it’s not concurrent-mode correct the way Jotai is and (2) does not have writable derived atoms, (3) has much more predictable stack formats since it’s in-repo with my application. Those reasons make it harder (for me) to build this for Jotai. Still, I wanted to share these ideas even if I don’t have time to write all the code.
@dai-shi I started experimenting with this a bit. There are two approaches to implementing this:
- add a lot more event callbacks to store (onAtomRead, onAtomWrite, and some kind of info to associate them). This keeps bundle size in store.ts lower but is much more confusing since there is more empty logic in store.ts to call these callbacks, and the logic will be brittle when it comes to version and other complexities.
- Build all the tracing logic into store.ts, and figure out how to code-eliminate it to avoid paying the bundle size cost in production. React does this by checking process.env.NODE_ENV without
typeof process, we could do the same thing? This would let us add extensive tracing right in store.ts in a much less brittle way, and in dev mode we can store debugging metadata right in store’s data structures.
React’s dead code elimination config for roll up: https://github.com/facebook/react/blob/fe905f152f1c9740279e31ce4478a5b8ca75172e/scripts/rollup/build.js#L372
@justjake Thanks for working on this!
I'm not super happy with adding complexity in store.ts, but if this is DEV-only thing (zero overhead in production), it's worthwhile. I'd put my effort on refactoring too.
typeof process is required for browser support. We need NODE_ENV check inline. Unlike React, we don't provide two builds. I know it's a bit annoying, but it's a different issue, and should be tackled separately.
can be added a 'devLabel' or so when create the atom? something like this:
const countAtom = atom(initValue, devlabel)
and take it to show in devtools?
We already have .debugLabel property in case you missed it.
const countAtom = atom(0);
countAtom.debugLabel = 'countAtom';
and, we do have a babel plugin to do it automatically behind the scene.
We already have
.debugLabelproperty in case you missed it.const countAtom = atom(0); countAtom.debugLabel = 'countAtom';and, we do have a babel plugin to do it automatically behind the scene.
great!!, yes I've missed it sorry, thanks for the info
hi, is it possible to hide an atom from redux store browser, I have a derived readonly atom that want to debug but don't want to see the updaters it introduce a lot of noise in the redux store browser
hi, is it possible to hide an atom from redux store browser, I have a derived readonly atom that want to debug but don't want to see the updaters it introduce a lot of noise in the redux store browser
Not at this point. It's on our action items. (cc: @arjunvegda btw, see OP, you might be interested.)
Meanwhile, a workaround could be using useAtomDevtools instead of useAtomsDevtools.
@cortesa Please open a new discussion and leave this issue for the original topic.
Moved to https://github.com/jotaijs/jotai-devtools/discussions/55