Mongez React Atom
A powerful, React-optimized state management library built on @mongez/atom. Provides React hooks, SSR support, and helper atoms for common patterns.
Table of Contents
Why React Atom?
React Atom extends @mongez/atom with React-specific features:
- React Hooks -
useValue(), useState(), use(), useWatch()
- SSR Support - Works with Next.js, Remix, and other SSR frameworks
- Helper Atoms - Pre-built atoms for common patterns (loading, fetching, portals)
- Zero Boilerplate - No providers, actions, or reducers needed
- Type-Safe - Full TypeScript support with inference
- Tiny Bundle - ~3KB gzipped
Note: Make sure to read @mongez/atom documentation first, as this package is a React adapter.
Installation
yarn add @mongez/react-atom
Or
npm i @mongez/react-atom
Or
pnpm add @mongez/react-atom
Quick Start
Get started in 30 seconds:
import { atom } from "@mongez/react-atom";
const counterAtom = atom({
key: "counter",
default: 0,
});
function Counter() {
const count = counterAtom.useValue();
return (
<button onClick={() => counterAtom.update(count + 1)}>
Count: {count}
</button>
);
}
That's it! The component re-renders automatically when the atom updates.
Architecture Overview
How It Extends @mongez/atom
React Atom is a thin wrapper around @mongez/atom that adds React-specific functionality:
@mongez/atom (Core)
↓
@mongez/react-atom (React Hooks)
What it adds:
- React hooks (
useValue, useState, use, useWatch)
- SSR Provider component
- Helper atoms (loading, fetching, portal, open)
- React-specific optimizations
What it inherits:
- All core atom methods (
update, merge, change, watch, etc.)
- Event system
- Atom actions
- Type safety
Hook-Based API
All React-specific features are exposed as methods on the atom:
const userAtom = atom({ key: "user", default: {} });
userAtom.useValue();
userAtom.useState();
userAtom.use("name");
userAtom.update(newUser);
userAtom.merge({ name: "John" });
userAtom.onChange(() => {});
This design makes atoms self-contained - everything you need is on the atom object.
Core Hooks
useValue()
Get the atom's value and re-render on changes:
function Header() {
const currency = currencyAtom.useValue();
return <div>Currency: {currency}</div>;
}
When to use:
- You only need to read the value
- You update the atom from event handlers
- You want the simplest API
useState()
Get value and updater function (like React's useState):
function Header() {
const [currency, setCurrency] = currencyAtom.useState();
return (
<>
<div>Currency: {currency}</div>
<button onClick={() => setCurrency("USD")}>USD</button>
<button onClick={() => setCurrency("EUR")}>EUR</button>
</>
);
}
When to use:
- You need both value and updater
- You're familiar with
useState API
- You want to pass updater to child components
Difference from useValue:
useValue() - Returns just the value
useState() - Returns [value, updater] tuple
use()
Watch a specific property of an object atom:
type User = {
name: string;
age: number;
notifications: number;
};
const userAtom = atom<User>({
key: "user",
default: {
name: "Hasan",
age: 25,
notifications: 0,
},
});
function Header() {
const notifications = userAtom.use("notifications");
return <div>Notifications: {notifications}</div>;
}
When to use:
- You only care about one property
- You want to avoid unnecessary re-renders
- The atom is an object with many properties
Performance benefit:
const user = userAtom.useValue();
return <div>{user.notifications}</div>;
const notifications = userAtom.use("notifications");
return <div>{notifications}</div>;
useWatch()
Execute a callback when a property changes:
function SomeComponent() {
const [city, setCity] = useState(userAtom.get("address.city"));
userAtom.useWatch("address.city", setCity);
return <div>Current City: {city}</div>;
}
When to use:
- You need to sync atom changes to local state
- You want to trigger side effects on property changes
- You're integrating with non-atom state
Important: The callback should be memoized (use useCallback or pass a setter function directly).
Creating Atoms
Basic Atom
import { atom } from "@mongez/react-atom";
export const currencyAtom = atom({
key: "currency",
default: "EUR",
});
Object Atom
type User = {
name: string;
email: string;
age: number;
};
export const userAtom = atom<User>({
key: "user",
default: {
name: "",
email: "",
age: 0,
},
});
Atom with Actions
export const counterAtom = atom({
key: "counter",
default: 0,
actions: {
increment() {
this.update(this.value + 1);
},
decrement() {
this.update(this.value - 1);
},
reset() {
this.update(0);
},
},
});
counterAtom.increment();
counterAtom.decrement();
counterAtom.reset();
Helper Atoms
React Atom provides pre-built atoms for common patterns.
openAtom
Manages open/closed state (modals, dropdowns, etc.):
import { openAtom } from "@mongez/react-atom";
export const loginModalAtom = openAtom("loginModal");
function LoginModal() {
const isOpen = loginModalAtom.useOpened();
return (
<Modal isOpen={isOpen} onClose={loginModalAtom.close}>
<div>Login Content</div>
</Modal>
);
}
<button onClick={loginModalAtom.open}>Login</button>;
API:
useOpened() - Hook to get/watch open state
open() - Set to true
close() - Set to false
toggle() - Toggle state
Default open:
const sidebarAtom = openAtom("sidebar", true);
loadingAtom
Manages loading state:
import { loadingAtom } from "@mongez/react-atom";
export const postsLoadingAtom = loadingAtom("postsLoading");
function Posts() {
const isLoading = postsLoadingAtom.useValue();
useEffect(() => {
postsLoadingAtom.startLoading();
fetchPosts()
.then(() => postsLoadingAtom.stopLoading())
.catch(() => postsLoadingAtom.stopLoading());
}, []);
if (isLoading) return <div>Loading...</div>;
return <div>Posts...</div>;
}
API:
startLoading() - Set to true
stopLoading() - Set to false
toggleLoading() - Toggle state
useValue() - Hook to get/watch state
fetchingAtom
Complete data fetching state management:
import { fetchingAtom } from "@mongez/react-atom";
type Post = {
id: number;
title: string;
body: string;
};
export const postsAtom = fetchingAtom<Post[]>("posts");
function Posts() {
const isLoading = postsAtom.useLoading();
const posts = postsAtom.useData();
const error = postsAtom.useError();
useEffect(() => {
postsAtom.startLoading();
fetchPosts()
.then((response) => {
postsAtom.success(response.data, response.pagination);
})
.catch((error) => {
postsAtom.failed(error.message);
});
}, []);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
{posts?.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</div>
);
}
API:
useLoading() - Hook for loading state
useData() - Hook for data
useError() - Hook for error
usePagination() - Hook for pagination
startLoading() - Start loading
stopLoading() - Stop loading
success(data, pagination?) - Mark as successful
failed(error) - Mark as failed
append(data) - Append to array data
prepend(data) - Prepend to array data
Pagination support:
postsAtom.success(posts, {
currentPage: 1,
totalPages: 10,
total: 100,
});
const pagination = postsAtom.usePagination();
Infinite scroll:
function loadMore() {
fetchNextPage().then((newPosts) => {
postsAtom.append(newPosts);
});
}
portalAtom
For modals/drawers that need to pass data:
import { portalAtom } from "@mongez/react-atom";
type EditUserData = {
userId: number;
initialName: string;
};
export const editUserPortal = portalAtom<EditUserData>("editUser");
function EditUserModal() {
const isOpen = editUserPortal.useOpened();
const data = editUserPortal.useData();
return (
<Modal isOpen={isOpen} onClose={editUserPortal.close}>
<div>Editing user {data.userId}</div>
<input defaultValue={data.initialName} />
</Modal>
);
}
<button
onClick={() =>
editUserPortal.open({
userId: 123,
initialName: "John",
})
}
>
Edit User
</button>;
API:
useOpened() - Hook for open state
useData() - Hook for data
open(data?) - Open with optional data
close() - Close
toggle(data?) - Toggle with optional data
SSR Support
React Atom works seamlessly with SSR frameworks.
Next.js App Router (13+)
Important: Add "use client" directive to atom files and components using atoms.
Creating Atoms
"use client";
import { atom } from "@mongez/react-atom";
type User = {
name: string;
email: string;
};
const userAtom = atom<User>({
key: "user",
default: {
name: "",
email: "",
},
});
export const UserAtomProvider = userAtom.Provider;
export default userAtom;
Using in Server Components
import { UserAtomProvider } from "@/atoms/user-atom";
import UserProfile from "@/components/UserProfile";
export default async function Page() {
const user = await fetchUser();
return (
<UserAtomProvider value={user}>
<UserProfile />
</UserAtomProvider>
);
}
Using in Client Components
"use client";
import userAtom from "@/atoms/user-atom";
export default function UserProfile() {
const user = userAtom.useValue();
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
Next.js Pages Router
import { UserAtomProvider } from "@/atoms/user-atom";
function MyApp({ Component, pageProps }) {
return (
<UserAtomProvider value={pageProps.user}>
<Component {...pageProps} />
</UserAtomProvider>
);
}
export default MyApp;
export async function getServerSideProps() {
const user = await fetchUser();
return {
props: {
user,
},
};
}
Remix
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { UserAtomProvider } from "@/atoms/user-atom";
import UserProfile from "@/components/UserProfile";
export async function loader() {
const user = await fetchUser();
return json({ user });
}
export default function Index() {
const { user } = useLoaderData<typeof loader>();
return (
<UserAtomProvider value={user}>
<UserProfile />
</UserAtomProvider>
);
}
AtomProvider
Create scoped atom instances (useful for lists or isolated contexts):
import { AtomProvider } from "@mongez/react-atom";
import { userAtom } from "@/atoms";
function UserList() {
const users = [
{ id: 1, name: "John" },
{ id: 2, name: "Jane" },
];
return (
<>
{users.map((user) => (
<AtomProvider key={user.id} register={[userAtom]}>
<UserCard initialData={user} />
</AtomProvider>
))}
</>
);
}
function UserCard({ initialData }) {
const scopedUserAtom = useAtom("user");
useEffect(() => {
scopedUserAtom.update(initialData);
}, []);
const user = scopedUserAtom.useValue();
return <div>{user.name}</div>;
}
Multiple atoms:
<AtomProvider register={[userAtom, settingsAtom, themeAtom]}>
<App />
</AtomProvider>
Working with Arrays
Use atomCollection for array-specific methods:
import { atomCollection } from "@mongez/react-atom";
type Todo = {
id: number;
text: string;
done: boolean;
};
const todosAtom = atomCollection<Todo>({
key: "todos",
default: [],
});
function TodoList() {
const todos = todosAtom.useValue();
const addTodo = (text: string) => {
todosAtom.push({
id: Date.now(),
text,
done: false,
});
};
const removeTodo = (id: number) => {
todosAtom.remove((todo) => todo.id === id);
};
return (
<div>
{todos.map((todo) => (
<div key={todo.id}>
{todo.text}
<button onClick={() => removeTodo(todo.id)}>Delete</button>
</div>
))}
</div>
);
}
See @mongez/atom documentation for all array methods.
Complete API Reference
Atom Creation
atom<Value, Actions>() | AtomOptions | ReactAtom | Creates a React atom |
atomCollection<Value>() | CollectionOptions | ReactAtom | Creates array atom |
openAtom() | key, defaultOpen? | ReactAtom | Creates open/close atom |
loadingAtom() | key, defaultLoading? | ReactAtom | Creates loading atom |
fetchingAtom<Data, Pagination>() | key, defaultValue?, defaultFetching? | ReactAtom | Creates fetching atom |
portalAtom<Data>() | key, defaultOpen? | ReactAtom | Creates portal atom |
React Hooks (on atom instance)
useValue() | Value | Get value and re-render on changes |
useState() | [Value, (value) => void] | Get value and updater |
use<K>(key) | Value[K] | Get property and re-render on its changes |
useWatch(key, callback) | void | Execute callback on property change |
SSR Components
atom.Provider | value, children | SSR provider for atom |
AtomProvider | register, defaultValue?, children | Scoped atom provider |
Context Hooks
useAtom<T>(key) | key: string | ReactAtom<T> | Get scoped atom from AtomProvider |
Helper Atom APIs
openAtom
useOpened() | boolean | Get/watch open state |
open() | void | Set to true |
close() | void | Set to false |
toggle() | void | Toggle state |
loadingAtom
useValue() | boolean | Get/watch loading state |
startLoading() | void | Set to true |
stopLoading() | void | Set to false |
toggleLoading() | void | Toggle state |
fetchingAtom
useLoading() | boolean | Get/watch loading |
useData() | Data | null | Get/watch data |
useError() | any | Get/watch error |
usePagination() | Pagination | undefined | Get/watch pagination |
startLoading() | void | Start loading |
stopLoading() | void | Stop loading |
success(data, pagination?) | void | Mark successful |
failed(error) | void | Mark failed |
append(data) | void | Append to array |
prepend(data) | void | Prepend to array |
portalAtom
useOpened() | boolean | Get/watch open state |
useData() | Data | Get/watch data |
open(data?) | void | Open with data |
close() | void | Close |
toggle(data?) | void | Toggle with data |
React 18+ Features
Concurrent Mode
Atoms work seamlessly with React 18's Concurrent Mode:
function UserProfile() {
const user = userAtom.useValue();
return <div>{user.name}</div>;
}
useTransition
Defer atom updates to keep UI responsive:
function SearchResults() {
const [isPending, startTransition] = useTransition();
const results = searchResultsAtom.useValue();
const handleSearch = (query: string) => {
startTransition(() => {
searchResultsAtom.update(performSearch(query));
});
};
return (
<div>
<input onChange={(e) => handleSearch(e.target.value)} />
{isPending && <div>Searching...</div>}
<Results data={results} />
</div>
);
}
useDeferredValue
Defer expensive re-renders:
function FilteredList() {
const items = itemsAtom.useValue();
const filter = filterAtom.useValue();
const deferredFilter = useDeferredValue(filter);
const filtered = useMemo(
() => items.filter((item) => item.includes(deferredFilter)),
[items, deferredFilter],
);
return <List items={filtered} />;
}
Suspense Integration
Wrap async atom updates with Suspense:
const userAtom = atom({
key: "user",
default: null,
actions: {
async load(userId: string) {
const user = await fetchUser(userId);
this.update(user);
},
},
});
function UserProfile({ userId }) {
const user = userAtom.useValue();
useEffect(() => {
userAtom.load(userId);
}, [userId]);
if (!user) throw new Promise(() => {});
return <div>{user.name}</div>;
}
<Suspense fallback={<div>Loading...</div>}>
<UserProfile userId="123" />
</Suspense>;
Advanced Patterns
Optimistic Updates
Update UI immediately, rollback on error:
const todosAtom = atomCollection<Todo>({ key: "todos", default: [] });
async function addTodo(text: string) {
const optimisticTodo = {
id: Date.now(),
text,
done: false,
};
todosAtom.push(optimisticTodo);
try {
const savedTodo = await api.createTodo(text);
todosAtom.replace(
todosAtom.index((t) => t.id === optimisticTodo.id),
savedTodo,
);
} catch (error) {
todosAtom.remove((t) => t.id === optimisticTodo.id);
alert("Failed to create todo");
}
}
Derived State
Create atoms that derive from others:
const firstNameAtom = atom({ key: "firstName", default: "John" });
const lastNameAtom = atom({ key: "lastName", default: "Doe" });
const fullNameAtom = atom({ key: "fullName", default: "" });
function useSyncFullName() {
const firstName = firstNameAtom.useValue();
const lastName = lastNameAtom.useValue();
useEffect(() => {
fullNameAtom.silentUpdate(`${firstName} ${lastName}`);
}, [firstName, lastName]);
}
function App() {
useSyncFullName();
const fullName = fullNameAtom.useValue();
return <div>{fullName}</div>;
}
Atom Selectors
Select and transform atom data:
const usersAtom = atomCollection<User>({ key: "users", default: [] });
function useActiveUsers() {
const users = usersAtom.useValue();
return useMemo(() => users.filter((user) => user.active), [users]);
}
function useUserById(id: number) {
const users = usersAtom.useValue();
return useMemo(() => users.find((user) => user.id === id), [users, id]);
}
Request Deduplication
Prevent duplicate API calls:
const requestCache = new Map<string, Promise<any>>();
const userAtom = atom({
key: "user",
default: null,
actions: {
async load(userId: string) {
const cacheKey = `user:${userId}`;
if (!requestCache.has(cacheKey)) {
requestCache.set(
cacheKey,
fetchUser(userId).finally(() => {
requestCache.delete(cacheKey);
}),
);
}
const user = await requestCache.get(cacheKey);
this.update(user);
},
},
});
Infinite Scroll
Implement infinite scroll with fetchingAtom:
const postsAtom = fetchingAtom<Post[]>("posts", []);
function InfinitePostList() {
const posts = postsAtom.useData();
const isLoading = postsAtom.useLoading();
const pagination = postsAtom.usePagination();
const loadMore = async () => {
if (isLoading || !pagination?.hasMore) return;
postsAtom.startLoading();
try {
const { data, pagination: newPagination } = await fetchPosts(
pagination.currentPage + 1,
);
postsAtom.append(data);
postsAtom.merge({ pagination: newPagination });
} catch (error) {
postsAtom.failed(error);
}
};
return (
<div>
{posts?.map((post) => (
<PostCard key={post.id} post={post} />
))}
{pagination?.hasMore && (
<button onClick={loadMore} disabled={isLoading}>
{isLoading ? "Loading..." : "Load More"}
</button>
)}
</div>
);
}
Performance Optimization
Preventing Unnecessary Re-renders
1. Use .use() for specific properties:
function Header() {
const user = userAtom.useValue();
return <div>{user.notifications}</div>;
}
function Header() {
const notifications = userAtom.use("notifications");
return <div>{notifications}</div>;
}
2. Split atoms by concern:
const appAtom = atom({
key: "app",
default: {
user: {},
theme: "light",
language: "en",
notifications: [],
},
});
const userAtom = atom({ key: "user", default: {} });
const themeAtom = atom({ key: "theme", default: "light" });
const languageAtom = atom({ key: "language", default: "en" });
const notificationsAtom = atomCollection({ key: "notifications", default: [] });
3. Separate triggering from listening:
function Posts() {
const posts = postsAtom.useData();
useEffect(() => {
postsAtom.startLoading();
fetchPosts().then((data) => postsAtom.success(data));
}, []);
return <div>{posts?.map(...)}</div>;
}
function PostsLoader() {
useEffect(() => {
postsAtom.startLoading();
fetchPosts().then((data) => postsAtom.success(data));
}, []);
return null;
}
function PostsList() {
const posts = postsAtom.useData();
return <div>{posts?.map(...)}</div>;
}
function Posts() {
return (
<>
<PostsLoader />
<PostsList />
</>
);
}
Memoization
Memoize expensive computations:
function FilteredList() {
const items = itemsAtom.useValue();
const filter = filterAtom.useValue();
const filtered = useMemo(
() => items.filter((item) => item.name.includes(filter)),
[items, filter],
);
return <List items={filtered} />;
}
Memoize components:
const PostCard = memo(function PostCard({ post }: { post: Post }) {
return <div>{post.title}</div>;
});
function PostsList() {
const posts = postsAtom.useData();
return (
<div>
{posts?.map((post) => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
Code Splitting
Split atoms by route:
export const postsAtom = fetchingAtom<Post[]>("posts");
export const usersAtom = fetchingAtom<User[]>("users");
const PostsPage = lazy(() => import("./routes/posts/PostsPage"));
const UsersPage = lazy(() => import("./routes/users/UsersPage"));
Testing Guide
Unit Testing Atoms
import { createAtom } from "@mongez/atom";
describe("counterAtom", () => {
let counterAtom;
beforeEach(() => {
counterAtom = createAtom({
key: "counter-test",
default: 0,
});
});
afterEach(() => {
counterAtom.destroy();
});
it("should have default value", () => {
expect(counterAtom.value).toBe(0);
});
it("should update value", () => {
counterAtom.update(5);
expect(counterAtom.value).toBe(5);
});
it("should notify listeners", () => {
const listener = jest.fn();
counterAtom.onChange(listener);
counterAtom.update(10);
expect(listener).toHaveBeenCalledWith(10, 0, counterAtom);
});
});
Testing Components with Atoms
Using React Testing Library:
import { render, screen, fireEvent } from "@testing-library/react";
import { atom } from "@mongez/react-atom";
import Counter from "./Counter";
const counterAtom = atom({ key: "counter-test", default: 0 });
describe("Counter", () => {
beforeEach(() => {
counterAtom.reset();
});
afterEach(() => {
counterAtom.destroy();
});
it("should display count", () => {
render(<Counter />);
expect(screen.getByText("Count: 0")).toBeInTheDocument();
});
it("should increment on click", () => {
render(<Counter />);
fireEvent.click(screen.getByText("Increment"));
expect(screen.getByText("Count: 1")).toBeInTheDocument();
});
});
Mocking Atoms
import { atom } from "@mongez/react-atom";
export const userAtom = atom({
key: "user-mock",
default: {
id: 1,
name: "Test User",
email: "test@example.com",
},
});
jest.mock("@/atoms", () => require("./__mocks__/atoms"));
test("renders user name", () => {
render(<UserProfile />);
expect(screen.getByText("Test User")).toBeInTheDocument();
});
Integration Testing
import { render, screen, waitFor } from "@testing-library/react";
import { postsAtom } from "@/atoms";
import PostsList from "./PostsList";
describe("PostsList Integration", () => {
beforeEach(() => {
postsAtom.reset();
});
it("should load and display posts", async () => {
render(<PostsList />);
expect(screen.getByText("Loading...")).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText("Post 1")).toBeInTheDocument();
});
expect(screen.queryByText("Loading...")).not.toBeInTheDocument();
});
});
TypeScript Patterns
Type-Safe Atoms
type User = {
id: number;
name: string;
email: string;
};
const userAtom = atom<User>({
key: "user",
default: {
id: 0,
name: "",
email: "",
},
});
const name = userAtom.use("name");
const id = userAtom.use("id");
userAtom.change("email", "test@example.com");
userAtom.change("email", 123);
Generic Helper Atoms
function createEntityAtom<T extends { id: number }>(
key: string,
defaultValue: T,
) {
return atom<T>({
key,
default: defaultValue,
actions: {
updateById(id: number, updates: Partial<T>) {
if (this.value.id === id) {
this.merge(updates);
}
},
},
});
}
const userAtom = createEntityAtom("user", {
id: 1,
name: "John",
email: "john@example.com",
});
userAtom.updateById(1, { name: "Jane" });
Discriminated Unions
type LoadingState =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: User }
| { status: "error"; error: string };
const userLoadingAtom = atom<LoadingState>({
key: "userLoading",
default: { status: "idle" },
});
function UserProfile() {
const state = userLoadingAtom.useValue();
switch (state.status) {
case "idle":
return <div>Click to load</div>;
case "loading":
return <div>Loading...</div>;
case "success":
return <div>{state.data.name}</div>;
case "error":
return <div>Error: {state.error}</div>;
}
}
Best Practices
1. Separate Triggering from Listening
function PostsLoader() {
useEffect(() => {
postsAtom.startLoading();
fetchPosts().then((data) => postsAtom.success(data));
}, []);
return null;
}
function PostsList() {
const posts = postsAtom.useData();
return <div>{posts?.map(...)}</div>;
}
2. Use Specific Hooks
const user = userAtom.useValue();
return <div>{user.notifications}</div>;
const notifications = userAtom.use("notifications");
return <div>{notifications}</div>;
3. Clean Up Subscriptions
userAtom.useValue();
useEffect(() => {
const sub = userAtom.onChange(() => {});
return () => sub.unsubscribe();
}, []);
4. Use Helper Atoms
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const dataAtom = fetchingAtom("data");
const isLoading = dataAtom.useLoading();
const data = dataAtom.useData();
const error = dataAtom.useError();
5. Type Your Atoms
const userAtom = atom({
key: "user",
default: {},
});
type User = { id: number; name: string };
const userAtom = atom<User>({
key: "user",
default: { id: 0, name: "" },
});
Troubleshooting
Component Not Re-rendering
Problem: Updating atom but component doesn't re-render.
Solutions:
const value = atom.value;
const value = atom.useValue();
- Pass new reference for objects:
atom.update(atom.value);
atom.update({ ...atom.value });
SSR Hydration Mismatch
Problem: "Hydration failed" error in Next.js.
Solution: Use atom Provider:
function Page() {
const user = userAtom.useValue();
return <div>{user.name}</div>;
}
export default function Page() {
const user = await fetchUser();
return (
<UserAtomProvider value={user}>
<UserProfile />
</UserAtomProvider>
);
}
"use client" Directive Missing
Problem: Error in Next.js App Router.
Solution: Add "use client" to atom files and components:
"use client";
import { atom } from "@mongez/react-atom";
export const userAtom = atom({ ... });
Memory Leaks
Problem: App slows down over time.
Solution: Hooks auto-cleanup, but manual subscriptions need cleanup:
userAtom.useValue();
useEffect(() => {
const sub = userAtom.onChange(() => {});
return () => sub.unsubscribe();
}, []);
Migration Guides
From Redux + React-Redux
Redux:
const initialState = { user: null };
function userReducer(state = initialState, action) {
switch (action.type) {
case "SET_USER":
return { user: action.payload };
default:
return state;
}
}
const user = useSelector((state) => state.user);
dispatch({ type: "SET_USER", payload: newUser });
React Atom:
const userAtom = atom({
key: "user",
default: null,
});
const user = userAtom.useValue();
userAtom.update(newUser);
Migration steps:
- Replace reducers with atoms
- Replace
useSelector with atom.useValue()
- Replace
dispatch with atom.update()
- Remove Redux provider
From Zustand
Zustand:
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);
React Atom:
const counterAtom = atom({
key: "counter",
default: 0,
actions: {
increment() {
this.update(this.value + 1);
},
},
});
const count = counterAtom.useValue();
counterAtom.increment();
Migration steps:
- Convert stores to atoms
- Move actions to atom actions
- Replace
useStore with atom hooks
From Jotai
Jotai:
const countAtom = atom(0);
const [count, setCount] = useAtom(countAtom);
React Atom:
const counterAtom = atom({
key: "counter",
default: 0,
});
const [count, setCount] = counterAtom.useState();
Migration steps:
- Add
key to atoms
- Replace
useAtom with atom.useState()
- Atoms work outside React (bonus!)
From Context API
Context:
const UserContext = createContext(null);
function App() {
const [user, setUser] = useState(null);
return (
<UserContext.Provider value={{ user, setUser }}>
<Child />
</UserContext.Provider>
);
}
function Child() {
const { user } = useContext(UserContext);
return <div>{user?.name}</div>;
}
React Atom:
const userAtom = atom({
key: "user",
default: null,
});
function App() {
return <Child />;
}
function Child() {
const user = userAtom.useValue();
return <div>{user?.name}</div>;
}
Benefits:
- No provider needed
- Works outside components
- Better performance
Real-World Examples
Shopping Cart
type CartItem = {
id: number;
name: string;
price: number;
quantity: number;
};
const cartAtom = atomCollection<CartItem>({
key: "cart",
default: [],
actions: {
addItem(item: Omit<CartItem, "quantity">) {
const existing = this.get((i) => i.id === item.id);
if (existing) {
this.replace(
this.index((i) => i.id === item.id),
{ ...existing, quantity: existing.quantity + 1 },
);
} else {
this.push({ ...item, quantity: 1 });
}
},
removeItem(id: number) {
this.remove((item) => item.id === id);
},
updateQuantity(id: number, quantity: number) {
const index = this.index((i) => i.id === id);
const item = this.get(index);
if (quantity === 0) {
this.remove(index);
} else {
this.replace(index, { ...item, quantity });
}
},
clear() {
this.update([]);
},
get total() {
return this.value.reduce(
(sum, item) => sum + item.price * item.quantity,
0,
);
},
},
});
function Cart() {
const items = cartAtom.useValue();
const total = useMemo(() => cartAtom.total, [items]);
return (
<div>
{items.map((item) => (
<div key={item.id}>
{item.name} - ${item.price} x {item.quantity}
<button onClick={() => cartAtom.removeItem(item.id)}>Remove</button>
</div>
))}
<div>Total: ${total}</div>
</div>
);
}
Authentication Flow
type User = {
id: number;
name: string;
email: string;
token: string;
};
const authAtom = atom<User | null>({
key: "auth",
default: null,
actions: {
async login(email: string, password: string) {
try {
const user = await api.login(email, password);
this.update(user);
localStorage.setItem("token", user.token);
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
},
logout() {
this.update(null);
localStorage.removeItem("token");
},
async loadFromStorage() {
const token = localStorage.getItem("token");
if (token) {
try {
const user = await api.verifyToken(token);
this.update(user);
} catch {
this.logout();
}
}
},
},
});
function LoginForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleSubmit = async (e) => {
e.preventDefault();
const result = await authAtom.login(email, password);
if (result.success) {
navigate("/dashboard");
} else {
alert(result.error);
}
};
return (
<form onSubmit={handleSubmit}>
<input value={email} onChange={(e) => setEmail(e.target.value)} />
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">Login</button>
</form>
);
}
function App() {
useEffect(() => {
authAtom.loadFromStorage();
}, []);
const user = authAtom.useValue();
if (!user) return <LoginForm />;
return <Dashboard />;
}
Multi-Step Wizard
type WizardStep = "personal" | "address" | "payment" | "review";
type WizardData = {
personal: { name: string; email: string };
address: { street: string; city: string };
payment: { cardNumber: string };
};
const wizardAtom = atom<{
currentStep: WizardStep;
data: Partial<WizardData>;
}>({
key: "wizard",
default: {
currentStep: "personal",
data: {},
},
actions: {
nextStep() {
const steps: WizardStep[] = ["personal", "address", "payment", "review"];
const currentIndex = steps.indexOf(this.value.currentStep);
if (currentIndex < steps.length - 1) {
this.change("currentStep", steps[currentIndex + 1]);
}
},
prevStep() {
const steps: WizardStep[] = ["personal", "address", "payment", "review"];
const currentIndex = steps.indexOf(this.value.currentStep);
if (currentIndex > 0) {
this.change("currentStep", steps[currentIndex - 1]);
}
},
updateData<K extends keyof WizardData>(step: K, data: WizardData[K]) {
this.merge({
data: {
...this.value.data,
[step]: data,
},
});
},
reset() {
this.update(this.defaultValue);
},
},
});
function Wizard() {
const currentStep = wizardAtom.use("currentStep");
return (
<div>
{currentStep === "personal" && <PersonalStep />}
{currentStep === "address" && <AddressStep />}
{currentStep === "payment" && <PaymentStep />}
{currentStep === "review" && <ReviewStep />}
</div>
);
}
Change Log
- V5.1.0 (Added Portal Atom)
- V5.0.0 (12 May 2024)
- React Atom now depends on @mongez/atom
- Refactored
openAtom, loadingAtom, and fetchingAtom to use Atom Actions
- Enhanced documentation
- V4.0.0 (10 Sept 2023)
- Added
register prop to AtomProvider
- Removed
useWatcher hook
use now accepts only the key
- V3.2.0 (31 Aug 2023)
- Enhanced Atom Provider for clone
- V3.1.0 (24 Jun 2023)
- Added
openAtom, loadingAtom, and fetchingAtom
- V3.0.0 (25 May 2023)
- V2.1.0 (21 Mar 2023)
- Added
merge method
- Enhanced
update typings
- V2.0.0 (18 Dec 2022)
- Removed legacy hooks
- Added
useState hook
- Enhanced atom typings