-
Hi! We're currently using Recoil and we're having really hard time with it. Our product is CRM-like so number of different atoms and logic is quite complex. Our main concern is that components should not be able to change particular atoms because data is very often distributed between different atoms and it should not be a component responsibility to understand these relations in logic. I couldn't find any helpful patterns or good practices in the Jotai documentation for such case. I was experimenting with const store = createStore();
const countAtom = atom(0);
async function incrementCountAtom() {
await wait(500);
store.set(countAtom, p => p + 1);
}
function App() {
const count = useAtomValue(countAtom);
return <button onClick={incrementCountAtom}>{count}</button>
} This is of course a very simple example but such action could potentially fetch some data and update multiple atoms. Would such approach work from your experience? Are there any other recommended ways? |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 9 replies
-
Store API is exposed for certain use cases, but I wouldn't recommend it in this case, because everything is within the React world. If I understand your problem correctly, the "Jotai" way is using write-only atoms. (It's just one practices and not all Jotai users are required to follow.) const countAtom = atom(0);
const incrementCountAtom = atom(null, async (get, set, arg) => {
await wait(500);
set(countAtom, p => p + 1);
});
function App() {
const count = useAtomValue(countAtom);
const incrementCount = useSetAtom(incrementCountAtom);
return <button onClick={incrementCount}>{count}</button>
} AFAIR, Recoil doesn't allow this pattern. |
Beta Was this translation helpful? Give feedback.
-
I'm migrating a project from recoil to jotai (which has been more pleasant than I thought so far), and I'm facing issues converting a pattern, where I have a recoil selector, which implements an interface to abstract away state management for a unit of functionality. const editorManager = selector({
key: "editor.manager",
get: ({ getCallback }): EditorManager => {
return {
openPath: getCallback(openPathCallback),
closeTab: getCallback(closeCallback),
closePath: getCallback(closePathCallback),
closeOtherTabs: getCallback(closeOtherTabsCallback)
};
},
}); This is useful because:
Since jotai doesn't have an equivalent of I can of course just export those callbacks in form of Another solution would be the write-only pattern suggested in this thread, but the caveats are already mentioned above. My main question is if there has been some thoughts about offering something similar to |
Beta Was this translation helpful? Give feedback.
-
I'm not super familiar with Recoil, but it sounds like you are asking for a pull-based approach to encapsulating all your business logic in the data layer. You can do this using import { atom } from 'jotai';
import { withAtomEffect } from 'jotai-effect';
import React from 'react';
import { useAtomValue } from 'jotai';
// ========================
// 1) Core State Atoms
// ========================
export const tabListAtom = atom<string[]>([]);
export const activeTabAtom = atom<string | null>(null);
export const logsAtom = atom<string[]>([]);
// ========================
// 2) One Atom (with Effect) per Editor Action
//
// IMPORTANT: Instead of doing `set(openTabAtom, openTab)`,
// we do `set(openTabAtom, { fn: openTab })`.
//
// Then, whenever we need to call that function, we destructure
// it, e.g. `const { fn } = get(openTabAtom)` or
// in a derived atom: `get(openTabAtom).fn(...)`.
// ========================
// 2.1) openTab
export const openTabAtom = withAtomEffect(
atom<{ fn: (path: string) => Promise<void> }>({ fn: async () => {} }),
(get, set) => {
const openTab = async (path: string) => {
// Simulate async operation
await new Promise((resolve) => setTimeout(resolve, 200));
const currentTabs = get(tabListAtom);
if (!currentTabs.includes(path)) {
set(tabListAtom, [...currentTabs, path]);
}
set(activeTabAtom, path);
set(logsAtom, (prev) => [...prev, `Tab opened: ${path}`]);
};
// Instead of `set(openTabAtom, openTab)`, do this:
set(openTabAtom, { fn: openTab });
return () => {
set(logsAtom, (prev) => [...prev, 'Cleanup: openTabAtom disposed']);
};
}
);
// 2.2) closeTab
export const closeTabAtom = withAtomEffect(
atom<{ fn: (path: string) => Promise<void> }>({ fn: async () => {} }),
(get, set) => {
const closeTab = async (path: string) => {
// Simulate side effect
await new Promise((resolve) => setTimeout(resolve, 100));
const currentTabs = get(tabListAtom);
set(tabListAtom, currentTabs.filter((t) => t !== path));
const active = get(activeTabAtom);
if (active === path) {
set(activeTabAtom, null);
}
set(logsAtom, (prev) => [...prev, `Tab closed: ${path}`]);
};
// Instead of `set(closeTabAtom, closeTab)`, do this:
set(closeTabAtom, { fn: closeTab });
return () => {
set(logsAtom, (prev) => [...prev, 'Cleanup: closeTabAtom disposed']);
};
}
);
// 2.3) closeOthers
export const closeOthersAtom = withAtomEffect(
atom<{ fn: (path: string) => Promise<void> }>({ fn: async () => {} }),
(get, set) => {
const closeOthers = async (path: string) => {
const currentTabs = get(tabListAtom);
set(tabListAtom, currentTabs.filter((t) => t === path));
set(activeTabAtom, path);
set(logsAtom, (prev) => [...prev, `Closed all tabs except: ${path}`]);
};
// Instead of `set(closeOthersAtom, closeOthers)`, do this:
set(closeOthersAtom, { fn: closeOthers });
return () => {
set(logsAtom, (prev) => [...prev, 'Cleanup: closeOthersAtom disposed']);
};
}
);
// 2.4) logMessage
export const logMessageAtom = withAtomEffect(
atom<{ fn: (message: string) => void }>({ fn: () => {} }),
(get, set) => {
const logMessage = (msg: string) => {
set(logsAtom, (prev) => [...prev, `Log: ${msg}`]);
};
// Instead of `set(logMessageAtom, logMessage)`, do this:
set(logMessageAtom, { fn: logMessage });
return () => {
set(logsAtom, (prev) => [...prev, 'Cleanup: logMessageAtom disposed']);
};
}
);
// ========================
// 3) Derived "EditorManager" Atom
// ========================
export interface EditorManager {
openTab: (path: string) => Promise<void>;
closeTab: (path: string) => Promise<void>;
closeOthers: (path: string) => Promise<void>;
logMessage: (message: string) => void;
}
export const editorManagerAtom = atom<EditorManager>((get) => ({
// Destructure the object's `fn` property instead of taking the entire object.
openTab: get(openTabAtom).fn,
closeTab: get(closeTabAtom).fn,
closeOthers: get(closeOthersAtom).fn,
logMessage: get(logMessageAtom).fn,
}));
// ========================
// 4) Usage in a React Component
// ========================
export function EditorUI() {
const manager = useAtomValue(editorManagerAtom);
const logs = useAtomValue(logsAtom);
const handleOpenTab = () => manager.openTab('my-file.txt');
const handleCloseTab = () => manager.closeTab('my-file.txt');
const handleCloseOthers = () => manager.closeOthers('my-file.txt');
return (
<div>
<button onClick={handleOpenTab}>Open Tab</button>
<button onClick={handleCloseTab}>Close Tab</button>
<button onClick={handleCloseOthers}>Close Others</button>
<h3>Logs:</h3>
<ul>
{logs.map((log, i) => (
<li key={i}>{log}</li>
))}
</ul>
</div>
);
} |
Beta Was this translation helpful? Give feedback.
Store API is exposed for certain use cases, but I wouldn't recommend it in this case, because everything is within the React world.
If I understand your problem correctly, the "Jotai" way is using write-only atoms. (It's just one practices and not all Jotai users are required to follow.)
AFAIR, Recoil doesn't allow this pattern.