@mongez/react-atom
v5.1.0
Published
A simple state management tool for React Js.
Downloads
255
Maintainers
Readme
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 value
Getting 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, merge
to update the atom's value, this will be a slightly better performance than usinguseState
hook.
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
change
method callsupdate
method 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
useCallback
hook.
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 client
directive 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 theopened
value.open
: a function that sets theopened
value totrue
.close
: a function that sets theopened
value tofalse
.toggle
: a function that toggles theopened
value.
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
loadingAtom
has same functions asopenAtom
, but instead ofopen
,close
andtoggle
, it hasstartLoading
,stopLoading
andtoggleLoading
.
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 theisLoading
value.startLoading
: a function that sets theisLoading
value totrue
.stopLoading
: a function that sets theisLoading
value tofalse
.useData
: a hook to get and watch for thedata
value.usePagination
: a hook to get and watch for thepagination
value, default value isnull
.useError
: a hook to get and watch for theerror
value.success
: A function that sets thedata
value and sets theisLoading
value tofalse
.failed
: A function that sets theerror
value and sets theisLoading
value 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
,loadingAtom
andfetchingAtom
functions to use Atom Actions. - Enhanced Documentation and removed any unrelated information to React Atom.
- V4.0.0 (10 Sept 2023)
- Added
register
prop toAtomProvider
component. - Removed
useWatcher
hook. use
now accepts only the key, to get the value useuseValue
hook instead.
- Added
- V3.2.0 (31 Aug 2023)
- Enhanced Atom Provider for clone.
- V3.1.0 (24 Jun 2023)
- Added
openAtom
,loadingAtom
andfetchingAtom
, functions.
- Added
- V3.0.0 (25 May 2023)
- Add Support or SSR.
- V2.1.0 (21 Mar 2023)
- Added
merge
method to atom. - Enhanced
update
typings. - Fixed
default
type to accept empty object. useWatcher
is now deprecated, useuse
instead.
- 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
useAtom
hook. - Removed
useAtomValue
hook. - Removed
useAtomState
hook. - Removed
useAtomWatch
hook. - Removed
useAtomWatcher
hook. - Removed
getAtomValue
function. - Removed
name
property from atom. - Removed
actions
. - Removed atom change debounce.
- Removed atom update debounce.
- Added
useState
hook 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)
beforeUpdate
now 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
useValue
method.
- V1.2.7 (25 July 2022)
- Fixed undefined bind value for object methods when called with
atom.get
method.
- 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
useWatcher
anduseWatch
embedded 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.