These computations could be avoided by using React's useMemo hook but that makes the code harder to read. Jotai's atoms were a much nicer way to get these computations to run only when the data changed.
// Simplified excerpt of the time entry atoms
// https://github.com/bashlk/timo/blob/main/packages/jotai/src/atoms/entryAtoms.js
import { atom } from 'jotai';
import { atomWithQuery } from 'jotai-tanstack-query';
const entriesAtom = atomWithQuery(() => ({
queryKey: ['entries'],
queryFn: listEntries,
}));
// This only recomputes when entriesAtom changes
const entriesGroupedByDateAtom = atom(
(get) => {
const { data } = get(entriesAtom);
if (data) {
return getEntriesGroupedByDate(data);
}
return {};
}
);
// This only recomputes when entriesGroupedByDateAtom change
const entriesDurationsGroupedByDateAtom = atom(
(get) => {
const entries = get(entriesGroupedByDateAtom);
if (entries) {
return Object.entries(entries).reduce((grouped, [date, dayEntries]) => {
grouped[date] = getTotalDuration(dayEntries);
return grouped;
}, {});
}
return {};
}
);
// Simplified excerpt of the reworked Entries screen
// https://github.com/bashlk/timo/tree/main/packages/jotai/src/routes/Entries
import { useAtomValue } from 'jotai';
import {
entriesGroupedByDateAtom,
entriesDurationsGroupedByDateAtom,
entriesDurationAtom,
} from '../../atoms/entryAtoms';
const Entries = ({ history }) => {
// These atoms are not recomputed during every render
const groupedEntries = useAtomValue(entriesGroupedByDateAtom);
const entriesDuration = useAtomValue(entriesDurationAtom);
const entriesDurationsGroupedByDate = useAtomValue(entriesDurationsGroupedByDateAtom);
return (
<div className={styles['body']}>
<div className={styles['total-row']}>
<h2 className={styles['total-label']}>Total</h2>
<div>{entriesDuration}</div>
</div>
{Object.entries(groupedEntries).map(([date, dayEntries]) => (
<div className={styles['day']} key={date}>
<div className={styles['day-header']}>
<h2 className={styles['day-name']}>{date}</h2>
<div>{entriesDurationsGroupedByDate[date]}</div>
</div>
{dayEntries.map((entry) => (
<Entry {...entry} />
))}
</div>
))}
</div>
)
}
I am conflicted about Jotai and atomic state management. On one hand, it provides a very effective way to move stateful logic outside of React components. But on the other, it is very easy to blow your leg off. A bad decision about how the atoms should be separated could lead to re-renders all across the app. The way how Jotai's atoms and hooks behave is not very obvious. Jotai's utilities and extensions are not described in full depth in the docs so some exploration is needed to figure out how to use them for more advanced scenarios.
So I can't recommend Jotai unless you really love the idea of working with atoms. It took me a while to get used to them since they need a different way of thinking than the common flux-like state management. In the next monthly post, I will be taking a look at another unconventional form of state management by trying out Valtio. If you are new here, you can sign up for the monthly posts so that you don't miss the next one.