Jotai (not exactly Redux) Dev Tools
Tags: reduxdevtools, debugging, jotai, timetravel
Published: 2020-10-28 22:22:42

Recently, we have begun investigating [Jotai](https://github.com/pmndrs/jotai), a state management alternative to [Recoil](https://recoiljs.org/) by [Poimandres](https://github.com/pmndrs). In addition, we documented some light investigation converting an existing Recoil app to Jotai in a [previous article](https://dev.to/c0d3t3k/recoil-vs-jotai-using-typescript-4678). While performing this exercise it got us thinking about what debug tools that might be available which are specifically targeted to Jotai. Unfortunately, as of this writing all that is available is the [useDebugValue](https://github.com/pmndrs/jotai/blob/master/docs/debugging.md) hook.

Now, like most folks, we enjoy using powerful and versatile dev tools such as [React DevTools](https://github.com/facebook/react/tree/master/packages/react-devtools), [React Query DevTools](https://github.com/tannerlinsley/react-query-devtools), etc. While we have not used [Redux](https://redux.js.org/) in a production capacity in the past, (for a number of reasons) we have always preferred to use [ReduxDevTools](https://github.com/reduxjs/redux-devtools) for debugging React state management systems. In fact, we have created these custom internal plugins for each state management system we have used in our contracting work. As of this writing, we couldn't find a Jotai plugin yet, so naturally we thought it might be interersting to attempt to create one.

<!-- ![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/zzmqwordkpmq81redykt.gif) -->
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/v65qonbuv1fgpke4lxlq.gif)

First, we setup some interfaces/types for Config, Message, ConnectionResult, Extension, etc. to match the payloads transmitted from/to ReduxDevTools. These interfaces enable strongly typed mapping for communication with the [Browser Extension](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd) proper.

```ts
interface Config {
    instanceID?: number,
    name?: string,
    serialize?: boolean,
    actionCreators?: any,
    latency?: number,
    predicate?: any,
    autoPause?: boolean
}

interface Message {
    type: string,
    payload?: any,
    state?: any
}

interface IConnectionResult {
    subscribe: (dispatch: any) => {};
    unsubscribe: () => {};
    send: (action: string, state: any) => {};
    error: (payload: any) => {};
}

type ConnectionResult = IConnectionResult & Observable<Message>

interface Extension {
    connect: (options?: Config) => ConnectionResult;
}
```

Unfortunately, the Redux DevTools connect function used for the browser extension frontend does not return a standard [RxJS](https://github.com/ReactiveX/rxjs) Observable. Consequently, we need the `wrapConnectionResult` function to make a RxJS compatible observable to receive ReduxDevTools events.

```ts
const wrapConnectionResult = (result: ConnectionResult) => {
    const subject = new Subject<Message>()

    result.subscribe(
        (x: any) => subject.next(x),
        (x: any) => subject.error(x),
        () => {subject.complete()}
    );

    return subject;
}
```


Now we'll implement a `JotaiDevtoolsProps` interface so that the user can name and configure DevTools instances.

```ts
interface JotaiDevtoolsProps {
    atom: WritableAtom<any, any>,
    name?: string,
    config?: Config
}
```

To enable seamless integration with React Functional Components, we create a hook that will take our JotaiDevtoolsProps and instantiate an instance for a particular Jotai atom.

```ts
...
export const useJotaiDevtools = ({ atom, name, config, ...props }: JotaiDevtoolsProps) => {
...
```

Next, we'll use the [`observable-hooks`](https://observable-hooks.js.org/) library's [`useObservable` hook](https://observable-hooks.js.org/api/#useobservable) to provide the value of the incoming atom as an RxJS observable.

```ts
    const atom$ = useObservable((input$) => input$.pipe(
        map(([x]) => x)
    ), [atomCurrentValue]);
```

In order to prevent a race condition when an atom is updated, we'll add a flag to determine whether the last state update was a ReduxDevTools *Time Travel Event*.

```ts
    const [wasTriggeredByDevtools, setWasTriggeredByDevtools] = useState(() => false);

```

Additionally, we need a flag to determine if the initial state has already been sent to the DevTools Extension.

```ts
    const [sentInitialState, setSentInitialState] = useState(() => false);

    const [devTools, setDevTools] = useState<ConnectionResult>();
```

Since the DevTools connection may not be availiable when the hook is initialized, we will add another `useObservable` to provide a sanitized stream of events when the connection is ready.

```ts
    const devTools$ = useObservable((input$) => input$.pipe(
        filter(([x]) => !!x),
        switchMap(([x]) => wrapConnectionResult(x as ConnectionResult)),
        catchError((error, observable) => {
            console.error(error);
            return observable;
        })
    ), [devTools]); 
```

This function is called to handle State Jumps and Time Travel events.

```ts
    const jumpToState = (newState: any) => {
        setWasTriggeredByDevtools(true)
        // var oldState = atomCurrentValue();
        setAtom(newState);
        // setWasTriggeredByDevtools(false);
    };
```

Continuing our previous pattern, we will use `observable-hook`'s [useSubscription](https://observable-hooks.js.org/api/#usesubscription) hook to subscribe to the DevTools Extension events and respond appropriately to either either a START or DISPATCH action.

```ts
    useSubscription(devTools$, (message) => {
        switch (message.type) {
            case 'START':
                console.log("Atom Devtools Start", options.name, atomCurrentValue)
                if(!sentInitialState) {
                    // devTools.send("\"" + options.name + "\" - Initial State", atom.getState());
                    devTools?.send(name + " - Initial State", atomCurrentValue);
                    setSentInitialState(true);
                }
                return;
            case 'DISPATCH':
                if (!message.state) {
                    return;
                }
                switch (message.payload.type) {
                    case 'JUMP_TO_ACTION':
                    case 'JUMP_TO_STATE':
                        jumpToState(JSON.parse(message.state));
                        return;
                }
                return;
        }
    });
```

Next, we will need another `useSubscribe` hook to subscribe updates coming from application state changes to the current Jotai atom.

```ts
    useSubscription(atom$, (state) => {
        if (wasTriggeredByDevtools) {
            setWasTriggeredByDevtools(false);
            return;
        }
        devTools?.send(name + " - " + moment().toISOString(), state);
    })
```

When the component is initialized, we will need a function to get a reference to the [DevTools Extension](https://github.com/reduxjs/redux-devtools). If the extension is not installed, an error will be logged to the console.

```ts
    const initDevtools = () => {
        const devtools = (window as any).__REDUX_DEVTOOLS_EXTENSION__ as Extension;
        // const options = config;
        const name = options.name || window.document.title;

        if (!devtools) {
            console.error('Jotai Devtools plugin: Cannot find Redux Devtools browser extension. Is it installed?');
            return atom;
        }

        const devTools = devtools.connect(options);
        
        console.log("Get Dev Tools", devTools, of(devTools));

        setDevTools(devTools);

        // setTimeout(() => devTools.send(name + " - Initial State", atomCurrentValue), 50)

        return atom;
    }
```

In order to activate our initialization function, we will utilize the `useLifeCycles` hook (from the excellent [react-use](https://github.com/streamich/react-use/blob/master/docs/useLifecycles.md) library) to handle the component mount lifecycle event.

```ts
    useLifecycles(() => {
        initDevtools();
    });
```

That's it. Now just install the new [Jotai Devtools plugin](https://github.com/c0d3t3k/jotai-devtools) in any projects that utilize [Jotai atoms](https://github.com/pmndrs/jotai#usetheatom).

Specifically, we can just call the `useJotaiDevtools` hook for each atom you want to view in the [Devtool Browser Extension](https://github.com/reduxjs/redux-devtools)


```ts
...
    useJotaiDevtools({
        name: "Dark Mode",
        atom: darkModeState
    });
...

    useJotaiDevtools({
        name: "Tasks",
        atom: tasksAtom
    });

...

```
For illustration, we can re-use the [Recoil](https://recoiljs.org/) example we converted to [Jotai](https://github.com/pmndrs/jotai) in a [previous post](https://dev.to/c0d3t3k/recoil-vs-jotai-using-typescript-4678). Once the app is started, we can open the [Redux DevTools Browser Extension](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd) and we will be able to watch our state changes, time travel debug, etc.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/v65qonbuv1fgpke4lxlq.gif)

## Final Thoughts:

- By leveraging the existing work done on ReduxDevTools, we have access to a useful debugging aid without reinventing the wheel.
- Leveraging the `observable-hooks` and `react-use` libraries enabled clean and efficient ReduxDevTools integration.
- Adapting the time travel functionality of ReduxDevTools enables full replay of the Jotai chain of state.
- The result is a compelling insight into the Jotai atom lifecycle.
- Check out the [Jotai Devtools Repo on Github](https://github.com/c0d3t3k/jotai-devtools)!

Below is a functioning example. If you haven't already, make sure you install the [Redux DevTools extension](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd) to be able to see the state update.



{% codesandbox happy-shockley-35roh %}