DEV Community

Hasan Zohdy
Hasan Zohdy

Posted on

React Atom: A simple yet powerful React JS / React Native state management

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
});
Enter fullscreen mode Exit fullscreen mode

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>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

As you can see, the default value is just a plain string, the atom initiation requires two keys:

  1. key the atom's name, it must be unique for each atom, otherwise an error will be raised in the development environment.
  2. 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>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

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: {},
});
Enter fullscreen mode Exit fullscreen mode

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>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

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
            });
        }} />
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

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);
        }} />
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

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',
  };
});
Enter fullscreen mode Exit fullscreen mode

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',
});
Enter fullscreen mode Exit fullscreen mode

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',
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Packages & Libraries

Courses (Articles)

Top comments (0)