@mongez/react-atom v5.1.0
Mongez React Atom
A powerful state management tool for React Js.
This is a React Js adapter built on Mongez Atom.
Make sure to read Mongez Atom documentation first before using this package as this package is an adapter for React Js.
Installation
yarn add @mongez/react-atom
Or
npm i @mongez/react-atom
Or
pnpm add @mongez/react-atom
Using Atoms outside components
Atoms can be accessed outside components, this is useful when you want to use the atom's value in a function or a class, or even in a service.
By embracing the idea using atoms outside components, we can easily manage the data in a single place, this can help you update or fetch the current atom's value while you're not using it inside a component.
Creating New Atom
The main idea here is every single data that might be manipulated will be stored independently in a shape of an atom.
This will raise the power of single responsibility.
import { atom } from "@mongez/react-atom";
export const currencyAtom = atom({
key: "currency",
default: "EUR",
});Please note that all atoms are immutables, the default data will be kept untouched if it is an object or an array.
When creating a new atom, it's recommended to pass the atom's value type as a generic type to the atom function, this will help you use the atom's value in a type-safe way.
Using Atoms in components
Now the currencyAtom atom has only single value, from this point we can use it in anywhere in our application components or event outside components.
Header.tsx
import React from "react";
import { currencyAtom } from "~/src/atoms";
export default function Header() {
// get current currency value and re-render the component when currency is changed
const currency = currencyAtom.useValue();
return (
<>
<h1>Header</h1>
Currency: {currency}
</>
);
}Footer.tsx
import React from "react";
import { useAtom } from "@mongez/react-atom";
import { currencyAtom } from "~/src/atoms";
export default function Footer() {
const currency = currencyAtom.useValue();
return (
<>
<h1>Footer</h1>
You're using our application in {currency} Currency.
</>
);
}In our Header component we just display the current value of the currency, which is the default value in our atom EUR.
In the Footer component, we also displayed the current currency in a form of a message.
Now let's add some buttons to change the current currency from the header.
Header.tsx
import { useAtom } from "@mongez/react-atom";
import { currencyAtom } from "~/src/atoms";
export default function Header() {
return (
<>
<h1>Header</h1>
<button onClick={() => currencyAtom.update("EUR")}>EUR</button>
<button onClick={() => currencyAtom.update("USD")}>USD</button>
<button onClick={() => currencyAtom.update("EGP")}>EGP</button>
</>
);
}Once we click on any button of the three buttons, the currency will be changed in our atom, this will re-render the Header once the currency is changed.
Get atom value
Atom's value can be fetched in different ways, depends what are you trying to do.
For example, if you're using the atom outside a React component or you're using it inside a component but don't want to rerender the component when the atom's value changes, you can use the atom.value property.
// anywhere in your app
import { currencyAtom } from "~/src/atoms";
console.log(currencyAtom.value); // get current valueGetting atom value and watch for its changes
Another way to get the atom's value when you're inside a React component, we can use atom.useValue() to get the atom's value and also trigger a component rerender when the atom's value changes.
import React from "react";
import { currencyAtom } from "~/src/atoms";
export default function Header() {
const currency = currencyAtom.useValue();
return (
<>
<h1>Header</h1>
Currency: {currency}
</>
);
}Get atom value and update it
If you want to get the atom's value and update it at the same time, you can use atom.useState().
import React from "react";
import { currencyAtom } from "~/src/atoms";
export default function Header() {
const [currency, setCurrency] = currencyAtom.useState();
return (
<>
<h1>Header</h1>
Currency: {currency}
<button onClick={(e) => setCurrency("EUR")}>EUR</button>
<button onClick={(e) => setCurrency("USD")}>USD</button>
<button onClick={(e) => setCurrency("EGP")}>EGP</button>
</>
);
}Works exactly like useState hook, the first item in the returned array is the current value of the atom, the second item is a state updater for the atom's value.
The main difference here is when the atom's value is changed from any other place, this component will be rerendered automatically.
Use
the atom.use() hook receives a key of the atom's object, it returns the current value and also watch for that key changes, this will re-render the component when the key is changed.
This is a recommended way to not make any useless renders in your components if other keys in the atom object is changed, we need to watch only for the key we're interested in.
type User = {
name: string;
age: number;
position: "developer" | "designer" | "manager";
notifications: number;
};
const userAtom = atom<User>({
key: "user",
default: {
name: "Hasan",
age: 25,
position: "developer",
},
});
// now in any component
import userAtom from "./userAtom";
export function Header() {
const notifications = userAtom.use("notifications");
return <header>{notifications}</header>;
}This will only re-render the component when the notifications property changes.
Changing only single key in the atom's value
Instead of passing the whole object to the setUser function, we can pass only the key we want to change using atom.change function.
import React from "react";
import { userAtom } from "~/src/atoms";
export default function UserForm() {
const [user, setUser] = userAtom.useState();
return (
<>
<h1>User Form</h1>
<input
type="text"
value={user.name}
onChange={(e) => userAtom.change("name", e.target.value)}
/>
<input
type="text"
value={user.email}
onChange={(e) => userAtom.change("email", e.target.value)}
/>
</>
);
}It's recommended to use one of the atom update methods
update, change, mergeto update the atom's value, this will be a slightly better performance than usinguseStatehook.
This will change only the given key in the atom's value, and trigger a component rerender if the atom's value is used in the component.
Please note that
changemethod callsupdatemethod under the hood, so it will generate a new object.
Atom Watch Hook
In some scenarios, we may need to watch for a key in the atom's value object for change and perform an action inside a component, the atom.useWatch hook is the perfect way to achieve this.
export function SomeComponent() {
const [city, setCity] = useState(userAtom.get("address.city"));
userAtom.useWatch("address.city", setCity);
// first time will render New York then it will render Cairo
return <>Current City: {city}</>;
}Please make sure that the callback function is a memoized function, this will prevent the function from being recreated on each render, you can pass the set state function or wrap your custom const callback function with
useCallbackhook.
AtomProvider
Atom Provider allows you to use same atom in a scoped version, this is useful when you want to deal with an atom inside an array of objects, or using the same atom in multiple components in the same page but each atom handles different data.
Wrap the code that you want to use the atom inside it with AtomProvider, and pass the to the register prop
import { AtomProvider } from "@mongez/react-atom";
import { currencyAtom } from "~/src/atoms";
export default function MyComponent() {
return (
<AtomProvider register={[currencyAtom]}>
<ChildComponent />
</AtomProvider>
);
}Now to access any atom from any component wrapped inside AtomProvider component, you need to use useAtom hook.
import { useAtom } from "@mongez/react-atom";
export default function Page() {
const userAtom = useAtom("currency");
return (
<div>
<div>Value: {value}</div>
<button onClick={() => userAtom.change("name", "New Value")}>
Change Value
</button>
</div>
);
}The main difference here you get a copy of the atom by calling useAtom, this will ensure that data are separated from the original atom, you get a new copy of the atom.
You may also register multiple atoms at once.
import { AtomProvider } from "@mongez/react-atom";
import currentAtom from "./currentAtom";
import userAtom from "./userAtom";
export default function App() {
return (
<AtomProvider register={[currentAtom, userAtom]}>
<App />
</AtomProvider>
);
}Because atoms are auto registered when the atom's file is being imported (when declaring an atom), this happens when the atom is being imported, but now we are using useAtom instead of the atom itself, thus we need to register the atom as well.
The argument passed to the useAtom hook is the atom name.
SSR Support
Now atoms can lay in SSR environments like Nextjs, Remix, etc, but with a little bit of change.
To make sure that the atom's value is being updated in both client and server, we need to create a special atom provider from the atom itself.
// it is important to add the `usa client` directive
"use client";
// src/atoms/user-atom.ts
import { atom } from "@mongez/react-atom";
type User = {
name: string;
email: string;
age: number;
id: number;
};
const userAtom = atom<User>({
key: "user",
default: {},
});
// very important is to create the UserAtomProvider
export const UserAtomProvider = userAtom.Provider;We can not directly use userAtom.Provider in Nextjs as it will throw an error of not identifying it, so we need to export it in a separate const UserAtomProvider.
Any component that uses the atom must declare
use clientdirective at the top of the file because atoms uses React useState hook under the hood, and this hook is not available in the server side.
// src/app/page.tsx
import { UserAtomProvider } from "~/atoms/user-atom";
export default function Page() {
const userFromCookies = {};
return (
<UserAtomProvider value={userFromCookies}>
<OtherComponentsListHere />
</UserAtomProvider>
);
}Now you can use the userAtom as usual in any component, it will be updated in both client and server.
Helper Atoms
Helper atoms functions allow you to easily manage variant atoms that you would probably use in your app.
Portal Atom
Added in V5.1.0
The portal atom is mainly used when working with modals, drawers or any other component that requires a state management and data transfer from a component to any other component that is not in the same component.
import { portalAtom } from "@mongez/react-atom";
export const loginPortal = portalAtom("loginPopup");Now let's declare the LoginPopup component.
LoginPopup.tsx
import { loginPortal } from "./atoms";
export default function LoginPopup() {
const opened = loginPortal.useOpened();
return (
<Modal isOpen={opened} onClose={loginPortal.close}>
<div>Login Content Here</div>
</Modal>
);
}Import the LoginPopup in the layout component or any shared component across the app.
Layout.tsx
import LoginPopup from "./LoginPopup";
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<LoginPopup />
{children}
</>
);
}From any component, you can open the login popup using loginPortal.open() function, let's open it from the header component.
Header.tsx
import { loginPortal } from "./atoms";
export default function Header() {
return <button onClick={loginPortal.open}>Login</button>;
}Open Atom
The openAtom function is mainly used to manage an open state, this one is useful when working with modals, popups, etc.
import { openAtom } from "@mongez/react-atom";
export const loginPopupAtom = openAtom("openAtom");This atom exposes 4 actions
useOpened: a hook to get and watch for theopenedvalue.open: a function that sets theopenedvalue totrue.close: a function that sets theopenedvalue tofalse.toggle: a function that toggles theopenedvalue.
By default, opened is set to false, if you want to set it to true by default, pass true as the second argument to booleanAtom function.
import { openAtom } from "@mongez/react-atom";
export const loginPopupAtom = openAtom("loginPopup", true);Let's see an example of usage
LoginPopup.tsx
import { loginPopupAtom } from "./atoms";
export default function LoginPopup() {
const opened = loginPopupAtom.useOpened(); // watch for opened when it is changed
return (
<Modal isOpen={opened} onClose={loginPopupAtom.close}>
<div>Login Content Here</div>
</Modal>
);
}Using open action to open the popup:
Header.tsx
import { loginPopupAtom } from "./atoms";
export default function Header() {
return (
<div>
<button onClick={loginPopupAtom.open}>Login</button>
</div>
);
}This applies to close and toggle functions as well.
Loading Atom
Another good helper function is loadingAtom which is used to manage a loading state, this is useful when you want to show a loading indicator when a request is being made.
It has 3 actions:
startLoading: a function that sets the atom value totrue.stopLoading: a function that sets the atom value tofalse.toggleLoading: a function that toggles the atom value.
By default, atom value is set to false, if you want to set it to true by default, pass true as the second argument to loadingAtom function.
import { loadingAtom } from "@mongez/react-atom";
export const loadingPostsAtom = loadingAtom("loadingPosts", true);Let's see an example of usage
Posts.tsx
import { loadingPostsAtom } from "./atoms";
import { useEffect, useState } from "react";
import { loadPosts } from "./api";
export default function Posts() {
const [posts, setPosts] = useState([]);
const isLoading = loadingPostsAtom.useValue(); // watch for isLoading when it is changed
useEffect(() => {
loadingPostsAtom.startLoading();
loadPosts().then((response) => {
loadingPostsAtom.stopLoading();
setPosts(response.data.posts);
});
}, []);
return (
<div>
{isLoading && <div>Loading...</div>}
{posts.map((post) => (
<div>{post.title}</div>
))}
</div>
);
}The
loadingAtomhas same functions asopenAtom, but instead ofopen,closeandtoggle, it hasstartLoading,stopLoadingandtoggleLoading.
Fetching Atom
This helper atom is quiet good actually, it allows you to manage an API fetching, consider it a full atom that manages the loading state, the data, and the error.
It exposes 8 actions:
useLoading: a hook to get and watch for theisLoadingvalue.startLoading: a function that sets theisLoadingvalue totrue.stopLoading: a function that sets theisLoadingvalue tofalse.useData: a hook to get and watch for thedatavalue.usePagination: a hook to get and watch for thepaginationvalue, default value isnull.useError: a hook to get and watch for theerrorvalue.success: A function that sets thedatavalue and sets theisLoadingvalue tofalse.failed: A function that sets theerrorvalue and sets theisLoadingvalue tofalse.append: A function that works only if data isarray, it appends the new data to the end of array.prepend: A function that works only if data isarray, it prepends the new data to the beginning of array.
Let's use the previous example of posts but this time with fetchingAtom
src/atoms/posts-atom.ts
import { fetchingAtom } from "@mongez/react-atom";
export type Post = {
id: number;
title: string;
body: string;
};
// define the post type as an array for better type checking
export const postsAtom = fetchingAtom<Post[]>("posts");Our atom is ready to be used, let's use it in our Posts component
src/components/Posts.tsx
import { postsAtom } from "../atoms/posts-atom";
import { useEffect } from "react";
export default function Posts() {
const isLoading = postsAtom.useLoading(); // watch for isLoading when it is changed
const data = postsAtom.useData(); // watch for data when it is changed
const error = postsAtom.useError(); // watch for error when it is changed
useEffect(() => {
postsAtom.startLoading();
loadPosts()
.then((response) => {
postsAtom.success(response.data.posts, response.data.pagination);
})
.catch((error) => {
postsAtom.failed(error);
});
}, []);
return (
<div>
{isLoading && <div>Loading...</div>}
{data && data.map((post) => <div>{post.title}</div>)}
{error && <div>{error.message}</div>}
</div>
);
}Best Practices With Atoms
Atoms have two main objectives, a triggering atom update and a listening for changes, so it is always better to separate any component that is going to be only the updating component from the component that is going to listen for changes.
In the login example, we have put the loginPopup update in the Header component, when user clicks on the login button, it will trigger atom update but the Header component is not interested in listening for changes, it is only interested in triggering the update so it will not re-render, in the meanwhile, the LoginPopup component is interested in listening for changes, so it will re-render when the atom is updated.
Let's put this into action, in the fetchingAtom example, we used triggering and listening values in the same component, let's separate them.
src/components/Posts.tsx
import { postsAtom } from "../atoms/posts-atom";
import { useEffect } from "react";
import LoadingPosts from "./LoadingPosts";
import PostsList from "./PostsList";
import PostsError from "./PostsError";
export default function Posts() {
useEffect(() => {
postsAtom.startLoading();
loadPosts()
.then((response) => {
postsAtom.success(response.data.posts);
})
.catch((error) => {
postsAtom.failed(error);
});
}, []);
return (
<div>
<LoadingPosts />
<PostsList />
<PostsError />
</div>
);
}Now we have separated the triggering component from the listening components, this will make the Posts component only responsible for triggering the atom update, and the LoadingPosts, PostsList and PostsError components are only responsible for listening for changes.
Let's create these components
src/components/LoadingPosts.tsx
import { postsAtom } from "../atoms/posts-atom";
export default function LoadingPosts() {
const isLoading = postsAtom.useLoading(); // watch for isLoading when it is changed
if (!isLoading) {
return null;
}
return <div>Loading...</div>;
}src/components/PostsList.tsx
import { postsAtom } from "../atoms/posts-atom";
export default function PostsList() {
const data = postsAtom.useData(); // watch for data when it is changed
if (!data) {
return null;
}
return (
<div>
{data.map((post) => (
<div>{post.title}</div>
))}
</div>
);
}src/components/PostsError.tsx
import { postsAtom } from "../atoms/posts-atom";
export default function PostsError() {
const error = postsAtom.useError(); // watch for error when it is changed
if (!error) {
return null;
}
return <div>{error.message}</div>;
}Using this approach, Posts component will not re-render when the atom is updated, this will make it render only once, each other component will be rendered for first time, then based on the atom changes, each component will start interacting.
For example the LoadingPosts component will be rendered for first time, then when calling startLoading method, it will re-render again, but the Posts component will not re-render because it is not listening for isLoading changes.
Working with Arrays
Mongez React Atom provides same collectAtom function to work with arrays in React.
import { collectAtom } from "@mongez/react-atom";
export const postsAtom = collectAtom<Post[]>("posts", []);Now a simple usage of the postsAtom atom
import { postsAtom } from "~/src/atoms";
export default function Posts() {
const posts = postsAtom.useValue();
return (
<div>
{posts.map((post) => (
<div>{post.title}</div>
))}
</div>
);
}Add item to the array
import { postsAtom } from "~/src/atoms";
export default function AddPost() {
const [title, setTitle] = useState("");
const [body, setBody] = useState("");
const addPost = () => {
postsAtom.push({
title,
body,
});
};
return (
<div>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<input
type="text"
value={body}
onChange={(e) => setBody(e.target.value)}
/>
<button onClick={addPost}>Add Post</button>
</div>
);
}Change Log
- V5.0.0 (12 May 2024)
- React Atom now depends on Mongez Atom package.
- Refactored
openAtom,loadingAtomandfetchingAtomfunctions to use Atom Actions. - Enhanced Documentation and removed any unrelated information to React Atom.
- V4.0.0 (10 Sept 2023)
- Added
registerprop toAtomProvidercomponent. - Removed
useWatcherhook. usenow accepts only the key, to get the value useuseValuehook instead.
- Added
- V3.2.0 (31 Aug 2023)
- Enhanced Atom Provider for clone.
- V3.1.0 (24 Jun 2023)
- Added
openAtom,loadingAtomandfetchingAtom, functions.
- Added
- V3.0.0 (25 May 2023)
- Add Support or SSR.
- V2.1.0 (21 Mar 2023)
- Added
mergemethod to atom. - Enhanced
updatetypings. - Fixed
defaulttype to accept empty object. useWatcheris now deprecated, useuseinstead.
- Added
- V2.0.1 (04 Jan 2023)
- Fixed atom typings when using anything that is not an object.
- V2.0.0 (18 Dec 2022)
- Removed
useAtomhook. - Removed
useAtomValuehook. - Removed
useAtomStatehook. - Removed
useAtomWatchhook. - Removed
useAtomWatcherhook. - Removed
getAtomValuefunction. - Removed
nameproperty from atom. - Removed
actions. - Removed atom change debounce.
- Removed atom update debounce.
- Added
useStatehook to atom. - Enhanced
atom typings.
- Removed
- V1.6.0 (14 Dec 2022)
- Added use method: Use atom's value or single value in a callback function.
- Enhanced types for objects.
- V1.5.0 (25 Sept 2022)
- Added Atom Actions
- Enhanced Atom Update Consistency
- V1.4.1 (01 August 2022)
beforeUpdatenow receives the old value as second argument and the atom object as third argument.
- V1.4.0 (31 July 2022)
- Added atom.addItem method: Add new item to the atom.
- Added atom.removeItem method: Add new item to the atom.
- Added atom.replaceItem method: update item in the atom's array.
- Added atom.getItem method: Get an item from the atom's array.
- Added atom.getItemIndex method: Get item index from the atom's array.
- Added atom.map: Map over the atom's values and trigger an update over it.
- Added atom.length: Get the length of the atom.
- Added atom.type: Get the atom's value type.
- V1.3.0 (28 July 2022)
- Fixed checking bind on null values.
- Added
useValuemethod.
- V1.2.7 (25 July 2022)
- Fixed undefined bind value for object methods when called with
atom.getmethod.
- Fixed undefined bind value for object methods when called with
- V1.2.6 (25 July 2022)
- Fixed return type of
Atom.useWatcher - V1.2.5 (25 July 2022)
- Added
useWatcheranduseWatchembedded in the atom itself.
- Added
- V1.2.4 (6 July 2022)
- Enhanced Atom Watcher.
- V1.2.3 (01 July 2022)
- Enhanced Atom Hooks.
- V1.2.2 (09 Jun 2022)
- Enhanced Atom Watcher.
- V1.2.1 (16 Apr 2022)
- Added get handler function.
- Disallowed triggering update/changes if called multiple times in the same time.
- V1.2.0 (25 Apr 2022)
- Added atom.watch Function feature.
- Added Atom.get Function.
- Added Atom.change Function.
- Added useAtomWatcher Hook.
- Added useAtomWatch Hook.
- V1.1.0 (25 Apr 2022)
- Added beforeUpdate function.
12 months ago
1 year ago
1 year ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
4 years ago
3 years ago
4 years ago
4 years ago
4 years ago