React Atom: A simple yet powerful React JS / React Native state management
Hasan Zohdy
Posted on March 21, 2023
Mongez React Atom
Mongez React Atom (MRA) is a state management tool to manage data between Project's Components
Why?
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.
Features
The top features of MRA is:
- Lightweight package
- Can be used everywhere, inside or outside React Components.
- Easy to use.
- Easy to learn.
- Power of focus, works with single value only.
- Typescript support.
- Can be used with any other state management tool or even better, use it as a replacement for Redux!
- Works with React JS and React Native.
Use Cases
The main purpose of a state management is to keep and maintain data, MRA is designed to work with a single value, this gives it a power of control to manage the data and keep it in a single place.
How it works
The concept is simple, everything works in atoms
, each atom MUST
hold a single value, in nutshell every atom has a single responsibility.
Installation
Yarn
yarn add @mongez/react-atom
NPM
npm i @mongez/react-atom
Pnpm
pnpm i @mongez/react-atom
Usage
Let's assume we have a currency to be displayed in the header, the footer contain a dropdown to change currency from it, we need to update the header's current currency once the user changes it in the footer without re-rendering the other components in same level or even the parent component that holds the header and footer.
Let's create a new simple atom
src/atoms/currency-atom.ts
import { atom } from '@mongez/react-atom';
export const currencyAtom = atom({
key: 'currency',
default: 'USD', // default value for the atom
});
Let's jump to use it in our Header
component
src/components/Header.tsx
import { currencyAtom } from 'src/atoms/currency-atom';
export default function Header() {
// get the current value of the atom
const currentCurrency = currencyAtom.value;
return (
<>
<div>Current Currency: {currentCurrency}</div>
</>
);
}
As you can see, the default
value is just a plain string, the atom initiation requires two keys:
-
key
the atom's name, it must be unique for each atom, otherwise an error will be raised in the development environment. -
default
holds the default value, it can be any type.
Now let's jump to the Footer
component
src/components/Footer.tsx
import { currencyAtom } from 'src/atoms/currency-atom';
export default function Footer() {
return (
<>
<button onClick={() => currencyAtom.update('EUR')}>Change to EUR</button>
<button onClick={() => currencyAtom.update('USD')}>Change to USD</button>
<button onClick={() => currencyAtom.update('GBP')}>Change to GBP</button>
<button onClick={() => currencyAtom.update('EGP')}>Change to EGP</button>
</>
);
}
This will update the currency atom value, but there will no be any re-rendering for the Header
component, because it's not subscribed to the atom, but if we want to re-render the Header
component once the currency is changed, we can use useValue
hook.
src/components/Header.tsx
import { currencyAtom } from 'src/atoms/currency-atom';
export default function Header() {
// get the current value of the atom
// and watch for any changes for the atom value
// if the atom's value is changed, re-render the component with the new value
const currentCurrency = currencyAtom.useValue();
return (
<>
<div>Current Currency: {currentCurrency}</div>
</>
);
}
Now whenever the user clicks on any of the buttons in the footer, the header will be re-rendered with the new value.
You can find an example code here in CodeSandbox
Working with objects
The previous example was a simple one, now let's try to work with objects, which will be the most use cases in your application, for example, we can use the atom to handle current user data throughout the entire application life cycle.
// src/atoms/user-atom.ts
import { atom } from '@mongez/react-atom';
export type UserData = {
id: number;
name: string;
email: string;
};
export const userAtom = atom<UserData>({
key: 'user',
default: {},
});
Here we defined a type for the data UserData
, we passed it to the atom so we can work with proper typings when using the atom in the application,
Now let's head back again to our header and add the current user name
src/components/Header.tsx
import { userAtom } from 'src/atoms/user-atom';
export default function Header() {
// get the current value of the atom
// and watch for any changes for the atom value
// if the atom's value is changed, re-render the component with the new value
const currentUser = userAtom.useValue();
const userName = currentUser.name;
return (
<>
<div>{userName}</div>
</>
);
}
Here we are listening to the atom's change, once the atom's value is updated then the component will be re-rendered with the new value.
Now let's add an input to change the user name in the footer
src/components/Footer.tsx
import { userAtom } from 'src/atoms/user-atom';
export default function Footer() {
return (
<>
<input defaultValue={userAtom.get('name')} onChange={e => {
userAtom.update({
...userAtom.value,
name: e.target.value
});
}} />
</>
)
}
Here we are using get
method to get the current value of the atom, then we are updating the atom's value with the new value, but we are not using update
method, because it will replace the entire value, we are using update
method to update the value with the new value, but we are keeping the old values.
get
method works only with objects, it will return the value of the given key from the object.
This will cause the header to be re-rendered with the new value.
We can use change
method to change single key's value
src/components/Footer.tsx
import { userAtom } from 'src/atoms/user-atom';
export default function Footer() {
return (
<>
<input defaultValue={userAtom.get('name')} onChange={e => {
userAtom.change('name', e.target.value);
}} />
</>
)
}
change
method works only with objects, it will update the value of the given key from the object.
Listen for certain changes
That was great so far, but what if the email
is changed, the Header
component has nothing to do with the email, it needs only the name thus it will cause unnecessary re-rendering when the atom's value is changed, thankfully we can listen for certain changes only using use
hook.
src/components/Header.tsx
import { userAtom } from 'src/atoms/user-atom';
export default function Header() {
// get and listen for changes only for the name key
const userName = userAtom.use('name');
return (
<>
<div>{userName}</div>
</>
);
}
This will cause the header to be re-rendered only when the name is changed, but what if we want to listen for multiple keys, we can use useMany
hook.
Even though the atom's object reference is changed, this will not cause any re-rendering, because the header is listening only for the name not the entire object.
Atom update
As we saw earlier we can update the atom's value using update
method, there is also another way to update the atom, same as the state updater
by passing a callback function to the update
method.
import { userAtom } from 'src/atoms/user-atom';
userAtom.update((oldValue) => {
return {
...oldValue,
name: 'Ali',
};
});
Partial updates
When dealing with objects or arrays, To make the atom trigger the change event, we must pass a new reference to the atom's update method, same as useState
hook, in that sense, we need to spread the old value along with the updated keys to make the atom trigger the change event.
import { userAtom } from 'src/atoms/user-atom';
userAtom.update({
...userAtom.value,
name: 'Ali',
});
This is the way to tell the atom the value has been changed, it is standard and easy to do, but we can use merge
method as a shortcut to do the same thing.
import { userAtom } from 'src/atoms/user-atom';
userAtom.merge({
name: 'Ali',
});
Kindly note that
merge
method works only with objects.
Before update
Sometimes you may need to make a modification for the data before updating it in the atom or even do other stuff, for example, you may need to make a request to the server to update the data, then you can use beforeUpdate
method to do that.
// src/atoms/user-atom.ts
import { atom } from '@mongez/react-atom';
export type UserData = {
id: number;
name: string;
email: string;
initial: string;
};
export const userAtom = atom<UserData>({
key: 'user',
default: {},
beforeUpdate: (newValue: UserData) => {
newValue.initial = newValue.name[0];
return newValue;
}
});
// src/components/Header.tsx
import { userAtom } from 'src/atoms/user-atom';
userAtom.update({
...userAtom.value,
name: 'Ali',
});
console.log(userAtom.get('initial')); // A
Here we used beforeUpdate
to modify the value before storing it in the atom.
Conclusion
We can recap the atom's methods as follows:
-
update
to update the atom's value -
merge
to update the atom's value with partial updates -
use
to listen for changes for a certain key -
useValue
to listen for changes for the entire atom's value -
get
to get the value key of the atom -
change
to change the value of a key in the atom -
beforeUpdate
to modify the value before updating it in the atom
You can find the documentation for package in Github
😍 Join our community
Join our community on Discord to get help and support (Node Js 2023 Channel).
📚 Bonus Content 📚
You may have a look at these articles, it will definitely boost your knowledge and productivity.
General Topics
- Event Driven Architecture: A Practical Guide in Javascript
- Best Practices For Case Styles: Camel, Pascal, Snake, and Kebab Case In Node And Javascript
- After 6 years of practicing MongoDB, Here are my thoughts on MongoDB vs MySQL
Packages & Libraries
- Collections: Your ultimate Javascript Arrays Manager
- Supportive Is: an elegant utility to check types of values in JavaScript
- Localization: An agnostic i18n package to manage localization in your project
Courses (Articles)
Posted on March 21, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.