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.
You can also register
atoms in the provider using register prop, it receives an array of atoms
import { AtomProvider } from "@mongez/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.
Added in V3.1.0
Helper atoms functions allow you to easily manage variant
atoms that you would probably use in your app.
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/atom";
export const loginPopupAtom = openAtom("openAtom");
This atom exposes 4 values:
opened
: boolean value that indicates if the popup is opened or not.open
: a function that sets the opened
value to true
.close
: a function that sets the opened
value to false
.toggle
: a function that toggles the opened
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/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.use("opened"); // watch for opened when it is changed
const close = loginPopupAtom.get("close"); // use `get` not `use` function to get the function
return (
<Modal isOpen={opened} onClose={close}>
<div>Login Content Here</div>
</Modal>
);
}
Header.tsx
import { loginPopupAtom } from "./atoms";
export default function Header() {
const openLoginPopup = loginPopupAtom.get("open"); // use `get` not `use` function to get the function
return (
<div>
<button onClick={openLoginPopup}>Login</button>
</div>
);
}
As you can see in the above example, we used get
function to get the open
and close
functions, this is because we don't want to watch for these functions, they are static functions, no changes will occur to them.
The opened
value is watched for changes, so when the popup is opened or closed, the LoginPopup
component will be re-rendered.
This works exactly like a normal atom, but, we can go more easier by using the atom actions directly, like open
, close
and toggle
.
import { loginPopupAtom } from "./atoms";
export default function Header() {
return (
<div>
<button onClick={loginPopupAtom.open}>Login</button>
</div>
);
}
So Before:
const open = loginPopupAtom.get("open");
After:
const open = loginPopupAtom.open;
This applies toclose
and toggle
functions as well.
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 values:
isLoading
: boolean value that indicates if the request is being made or not.startLoading
: a function that sets the isLoading
value to true
.stopLoading
: a function that sets the isLoading
value to false
.toggleLoading
: a function that toggles the isLoading
value.By default, isLoading
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/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.use("isLoading"); // 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
.
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
values:
isLoading
: boolean value that indicates if the request is being made or not, default value is false
.startLoading
: a function that sets the isLoading
value to true
.stopLoading
: a function that sets the isLoading
value to false
.data
: the data returned from the API, default value is null
.pagination
: the pagination returned from the API, default value is null
.error
: the error returned from the API, default value is null
.success
: A function that sets the data
value and sets the isLoading
value to false
.failed
: A function that sets the error
value and sets the isLoading
value to false
.append
: A function that works only if data is array
, it appends the new data to the existing data.prepend
: A function that works only if data is array
, it prepends the new data to the existing data.Let's use the previous example of posts but this time with fetchingAtom
src/atoms/posts-atom.ts
import { fetchingAtom } from "@mongez/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
import { postsAtom } from "../atoms/posts-atom";
import { useEffect } from "react";
export default function Posts() {
const isLoading = postsAtom.use("isLoading"); // watch for isLoading when it is changed
const data = postsAtom.use("data"); // watch for data when it is changed
const error = postsAtom.use("error"); // 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>
);
}
Again, the exposed functions are used only with the helper atoms, like fetchingAtom
, loadingAtom
and openAtom
.
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.use("isLoading"); // 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.use("data"); // 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.use("error"); // 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.
END OF THE GREATEST STATE MANAGEMENT IN THE WORLD
check more about it in Atom