A powerful state management tool for React Js.
The main purpose of the birth of this package is to work with a simple and performant state management tool to handle data among components and outside components.
Redux is a powerful state management tool, the purpose of this package is to use a simple state management which provides a good performance with large applications.
- Simple and easy to use
- Can be used in any React Js/React Native application.
- Can be used outside components.
- Listen to atom's value change.
- Listen to atom's object property change.
- Lightweight in size.
- Has very good utilities to manage atom if it is array or object.
yarn add @mongez/react-atom
Or
npm i @mongez/react-atom
Atoms are meant to be unique therefore the atom key
can not be used in more than one atom, if other atom is being created with a previously defined atom, an error will be thrown that indicates to use another atom key.
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.
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, Atom } from "@mongez/react-atom";
export const currencyAtom: Atom<string> = 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.
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() {
const currency = currencyAtom.use();
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.use();
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 React from "react";
import { useAtom } from "@mongez/react-atom";
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>
</>
);
}
Once we click on any button of the three buttons, the currency will be changed in our atom, the good thing here is it will be changed in the Footer
component as well.
Any atom must have a default
value when initializing it, this value can be any type, it can be a string, number, boolean, object, array, however, when the default value is an object
or an array
, the atom gets a special treatment.
We will see this later in the documentation.
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
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}
</>
);
}
The basic way to update atom's value is by using atom.update
, this method receives the new value of the atom and updates it.
// anywhere in your app
import { currencyAtom } from "~/src/atoms";
currencyAtom.update("USD"); // any component using the atom will be rerendered automatically.
We can also pass a callback to the update function, the callback will receive the old value and the atom instance.
// anywhere in your app
import { currencyAtom } from "~/src/atoms";
currencyAtom.update((oldValue, atom) => {
// do something with the old value
return "USD";
});
Please do remember that
atom.update
must receive a new reference of the value, otherwise it will not trigger the change event, for exampleatom.update({ ...user })
will trigger the change event.
// /src/atoms/user-atom.ts
import { atom } from "@mongez/react-atom";
export type UserData = {
name: string;
email: string;
age: number;
id: number;
};
export const userAtom = atom<UserData>({
key: "user",
default: {
name: "Hasan",
age: 30,
email: "[email protected]",
id: 1,
},
});
Now if we want to make an update for the user atom using atom.update
, it will be something like this:
// anywhere in your app
import { userAtom } from "~/src/atoms/user-atom";
userAtom.update({
...userAtom.value,
name: "Ahmed",
});
Or using callback to get the old value:
// anywhere in your app
import { userAtom } from "~/src/atoms/user-atom";
userAtom.update((oldValue) => {
return {
...oldValue,
name: "Ahmed",
};
});
Added in v2.1.0
If the atom is an object atom, you can use atom.merge
to merge the new value with the old value.
// src/atoms/user-atom.ts
import { atom } from "@mongez/react-atom";
export type UserData = {
name: string;
email: string;
age: number;
id: number;
};
export const userAtom = atom<UserData>({
key: "user",
default: {
name: "Hasan",
age: 30,
email: "[email protected]",
id: 1,
},
});
Now if we want to make an update for the user atom using atom.update
, it will be something like this:
// anywhere in your app
import { userAtom } from "~/src/atoms";
userAtom.update({
...userAtom.value,
name: "Ahmed",
age: 25,
});
If you notice, we've to spread the old value and then add the new values, this is good, but we can use atom.merge
instead.
// anywhere in your app
import { userAtom } from "~/src/atoms";
userAtom.merge({
name: "Ahmed",
age: 25,
});
This is just a shortcut for atom.update
, it will merge the new value with the old value and then update the atom.
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.
Another super amazing feature here is to watch for a property of the atom's value if it's defined as an object.
import React from "react";
import { atom, Atom } from "@mongez/react-atom";
type User = {
name: string;
email: string;
age: number;
};
const userAtom = atom<User>({
key: "user",
default: {},
});
Now let's create a component to display the user's name and email.
import React from "react";
import { userAtom } from "~/src/atoms";
export default function User() {
const user = userAtom.useValue();
return (
<>
<h1>User</h1>
<p>Name: {user.name}</p>
<p>Email: {user.email}</p>
</>
);
}
Now let's update the user's name from another component.
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) => setUser({ ...user, name: e.target.value })}
/>
<input
type="text"
value={user.email}
onChange={(e) => setUser({ ...user, email: e.target.value })}
/>
</>
);
}
This is great, but what if we want to have a component that will be rerendered only when the user's name changes, not the email, here we can use atom.useWatcher
to watch for a specific property of the atom's value.
import React from "react";
import { userAtom } from "~/src/atoms";
export default function User() {
const name = userAtom.useWatcher("name");
return (
<>
<h1>User</h1>
<p>Name: {name}</p>
</>
);
}
Now when the name
property is changed, this component will be rerendered automatically, otherwise it won't.
Using atom.use
will merge both useValue
and useWatcher
methods into one.
If the use
received a parameter, then it will be watching for the given property change, otherwise it will watch for the entire atom's value change.
Starting from version 2 and above,
atom.use
will be the recommended way to watch for atom's value changes for single property atoms instead ofuseWatcher
asuseWatcher
will be removed in the next release.
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.
Using use
without any parameter will watch for the entire atom's value change.
type User = {
name: string;
age: number;
position: "developer" | "designer" | "manager";
notifications: number;
};
// now in any component
import userAtom from "./userAtom";
export function Header() {
const user = userAtom.use();
return <header>{user.notifications}</header>;
}
This will be rerendered when the entire atom's value changes.
From V1.6.0
types are enhanced, when you pass the type
to the atom, then the use
method will return the same type, also it will allow only properties that are defined in the type.
type User = {
name: string;
age: number;
position: "developer" | "designer" | "manager";
notifications: number;
};
// now in any component
import userAtom from "./userAtom";
export function Header() {
const notifications = userAtom.use("notifications"); // will return number, and Typescript will complain if you try to use other properties
return <header>{notifications}</header>;
}
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)}
/>
</>
);
}
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.
If atom's value is an object, we can get a value from the atom directly using atom.get
function.
import { atom } from "@mongez/atom-react";
const userAtom = atom({
key: "user",
default: {
key: "Hasan",
address: {
city: "New York",
},
},
});
console.log(userAtom.get("key")); // Hasan
Dot Notation is also supported.
console.log(userAtom.get("address.city")); // New York
If key doesn't exist, return default value instead.
console.log(userAtom.get("email", "[email protected]")); // [email protected]
This feature might be useful in some scenarios when we need to reset the atom's value to its default value.
// anywhere in your app
import { currencyAtom } from "~/src/atoms";
currencyAtom.reset(); // any component using the atom will be rerendered automatically.
This will trigger an atom update and set the atom's value to its default value.
We can also destroy the atom using destroy()
method from the atom, this will stop re-rendering any component that using the atom using useAtom
or useAtomState
hooks.
// anywhere in your app
import { currencyAtom } from "~/src/atoms";
currencyAtom.destroy();
To get the atom key, use atom.key
will return the atom key.
// anywhere in your app
import { currencyAtom } from "~/src/atoms";
console.log(currencyAtom.key); // currencyAtom
To list all registered atoms, use atomsList
utility for that purpose.
// anywhere in your app
import { atomsList } from "~/src/atoms";
console.log(atomsList()); // [currencyAtom, ...]
Sometimes we may need to handle the atom.get
function to get the data in customized way, we can achieve this by defining in the atom function call how the atom will retrieve the object's value.
Without Defining the atom getter
const settingsAtom = atom({
key: "user",
default: {
isLoaded: false,
settings: {},
},
});
// later
settingsAtom.update({
isLoaded: true,
settings: {
websiteName: "My Website Name",
},
});
console.log(userAtom.get("settings.websiteName")); // My Website Name
After Defining it
import { atom } from "@mongez/atom-react";
const settingsAtom = atom({
key: "settings",
default: {
isLoaded: false,
settings: {},
},
get(key: string, defaultValue: any = null, atomValue: any) {
return atomValue[key] !== undefined
? atomValue[key]
: atomValue.settings[key] !== undefined
? atomValue.settings[key]
: defaultValue;
},
});
// later
settingsAtom.update({
isLoaded: true,
settings: {
websiteName: "My Website Name",
},
});
console.log(settingsAtom.get("websiteName")); // My Website Name
This is what happens with useAtom
hook, it listens to the atom's value change using onChange
method.
// anywhere in your app
import { currencyAtom } from "~/src/atoms";
currencyAtom.onChange((newValue, oldValue, atom) => {
//
});
Please note the
onChange
is returning an EventSubscription instance, we can remove the listener anytime, for example when unmounting the component.
// anywhere in your app
import { currencyAtom } from "~/src/atoms";
// in your component...
const [currency, setCurrency] = useState(currencyAtom.value);
useEffect(() => {
const onCurrencyChange = currencyAtom.onChange(setCurrency);
return () => onCurrencyChange.unsubscribe();
}, []);
Sometimes you may need to watch for only a key in the atom's value object, the atom.watch
function is the perfect way to achieve this.
Please note this only works if the atom's default is an object or an array.
// anywhere in your app
import { atom } from "@mongez/react-atom";
const userAtom = atom({
key: "user",
default: {
key: "Hasan",
address: {
city: "New York",
},
},
});
userAtom.watch("key", (newName, oldName) => {
console.log(newName, oldName); // 'Hasan', 'Ali'
});
// later in the app
userAtom.update({
...userAtom.value,
key: "Ali",
});
Dot notation is allowed too.
// anywhere in your app
import { atom } from "@mongez/react-atom";
const userAtom = atom({
key: "user",
default: {
key: "Hasan",
address: {
city: "New York",
},
},
});
userAtom.watch("address.cty", (newCity, oldCity) => {
console.log(newName, oldName); // 'New York', 'Cairo'
});
// later in the app
userAtom.update({
...userAtom.value,
address: {
...userAtom.value.address,
city: "Cairo",
},
});
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}</>;
}
Sometimes it's useful to mutate the value before updating it in the atom, this can be achieved via defining beforeUpdate
method in the atom declaration.
This is very useful especially when dealing with objects/arrays and you want to make some operations before using the final value.
beforeUpdate(newValue: any, oldValue: any, atom: Atom)
import { atom, Atom } from "@mongez/react-atom";
export const multipleAtom: Atom = atom({
key: "multiple",
default: 0,
beforeUpdate(newNumber: number): number {
return newNumber * 2;
},
});
multipleAtom.update(4);
console.log(multipleAtom.value); // 8
To detect atom destruction when destroy()
method, use onDestroy
.
// anywhere in your app
import { currencyAtom } from "~/src/atoms";
const subscription = currencyAtom.onDestroy((atom) => {
//
});
We can get the type of the atom's value using atom.type
property.
const currencyAtom = atom({
key: "currency",
default: "USD",
});
console.log(currencyAtom.type); // string
If the default value is an array it will be returned as array not object.
const todoListAtom = atom({
key: "todo",
default: [],
});
console.log(todoListAtom.type); // array
Works only if the atom's default value is array
We can get use of the following methods to make our life easier.
- Add Item add new item to the atom's array.
- Remove Item Remove item from the atom's array.
- Remove Items Remove items from the atom's array.
- Replace Item Update item'value in the atom's array.
- Get Item Get item from the atom's array.
- Get Item Index Get item' index from the atom's array.
- Items Map Map over the atom's array items and replace it with a new one.
- Items length Get the length of the atom's array.
atom.addItem(item: any) => void
This method will allow you adding item to the array, it will also trigger the change event.
const todoListAtom = atom({
key: "todo",
default: [],
});
// SomeComponent.tsx
export function TodoList() {
const items = todoListAtom.useValue();
const addNewItem = () =>
todoListAtom.addItem({
title: "My first task",
id: 213,
}); // this will update the items and re-render the component again.
return (
<>
Total Items: {items.length}
<button onClick={addNewItem}>Add Item</button>
</>
);
}
atom.removeItem(index: number | (item: any, index: number) => number) => void
To remove an item from the atom's array we can use the removeItem
method.
const todoListAtom = atom({
key: "todo",
default: [],
});
// SomeComponent.tsx
export function TodoList() {
const items = todoListAtom.useValue();
const addNewItem = () =>
todoListAtom.addItem({
title: "My first task",
id: 213,
}); // this will update the items and re-render the component again.
return (
<>
Total Items: {items.length}
<button onClick={addNewItem}>Add Item</button>
{items.map((item, index) => (
<div key={index}>
<div>Title: {item.title}</div>
<button onClick={() => todoListAtom.removeItem(index)}>Remove</button>
</div>
))}
</>
);
}
This will remove the item by the given index.
It can also be removed by passing a callback to remove the item from the list.
todoListAtom.removeItem((item) => item.id > 100);
Please note this will only remove the first matched item.
To remove multiple items, use removeItems
method instead.
atom.removeItem(indexes: number[] | (item: any, index: number) => number) => void
Works exactly like removeItem
except that it accepts an array of indexes or a callback function to remove multiple items.
todoListAtom.removeItems([0, 2, 3]); // will remove index 0, 2 and 3
// OR
todoListAtom.remoteItems((item) => item.id > 1);
atom.replaceItem(index: number, newItemValue: any) => void
Updates item's value by for the given index
const index = 2;
todoListAtom.replaceItem(2, {
title: "New Title",
});
atom.getItem(indexOrCallback: number | ((item: any, index: number) => any)) => any
Get an item from the array using item index or callback function.
const index = 2;
const item = todoListAtom.getItem(index);
// Or
const itemId = 15111; // dummy id
const otherItem = todoListAtom.getItem((item) => item.id === itemId);
atom.getItemIndex(callback: (item: any, index: number, array: any[]) => any) => any
Get the index of the first matched element to the given callback.
const itemId = 15111; // dummy id
const itemIndex = todoListAtom.getItemIndex((item) => item.id === itemId); // 2 for example
atom.map(callback: (item: any, index: number, array: any[]) => any) => any
Walk over every item in the array and update it, this will trigger the change event.
const numbersAtom = atom({
key: "number",
default: [1, 2, 3, 4],
});
// multiple the atom's array numbers by 2
numbersAtom.map((number) => number * 2);
console.log(numbersAtom.value); // [2, 4, 6, 8];
This can be useful feature when working with arrays or strings, atom.length
returns the count of total elements/characters of the atom's current value.
const todoListAtom = atom({
key: "todo",
default: [],
});
console.log(todoListAtom.length); // 0
todoListAtom.addItem({
title: "My first task",
id: 213,
});
console.log(todoListAtom.length); // 1
Added in V3.0.0
Now atoms can lay in SSR environments like Nextjs, Remix, etc, but with a little bit of change.
In your base app project, wrap your app with AtomProvider
component.
import { AtomProvider } from "@mongez/atom";
export default function App() {
return (
<AtomProvider>
<App />
</AtomProvider>
);
}
Then in your pages, wrap your page component with AtomProvider
component.
Now to access any atom from any component wrapped inside AtomProvider
component, you need to use useAtom
hook.
import { useAtom } from "@mongez/atom";
export default function Page() {
const userAtom = useAtom("user");
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 on each page request, you get a new copy of the atom, and the atom will be updated only for the current request.
Do not use the original atom inside SSR apps, use
useAtom
and pass to it the atom's key.
- 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.