* Illustration created by 沢庵
@pengoose/jotai
is a Convention manager
for managing state in React applications using Jotai. It provides a simple and structured way to define and manage your application's state, making it easier to organize and maintain your state logic. By following a set of conventions, you can create a consistent and scalable state management system that is easy to understand and maintain.
Install the package using npm:
npm install @pengoose/jotai
Or using yarn:
yarn add @pengoose/jotai
- Define the interfaces of the states you want to manage using atoms.
- Inject the interfaces into the generic type of the
AtomManager
. - Create a class that manages the state by inheriting the
AtomManager
abstract class.
// example/types.ts
export interface Music {
id: string;
title: string;
thumbnail: string;
url: string;
}
export interface PlaylistStatus {
playlist: Music[];
index: number;
}
- Extend the
AtomManager
class to create your state manager. - you have to implement selectors and actions in the
AtomManager
class.
import { atom } from 'jotai';
import { AtomManager } from '@pengoose/jotai';
import { PlaylistStatus, Music } from '@/types';
// 1. Extend AtomManager to create your state manager
export class Playlist extends AtomManager<PlaylistStatus> {
// 2. Implement selectors
public selectors = {
playlist: atom((get) => {
const { playlist } = get(this.atom);
return playlist;
}),
currentMusic: atom((get) => {
const { playlist, index } = get(this.atom);
return playlist[index];
}),
// ... other selectors
};
// 3. Implement actions
public actions = {
add: atom(null, (get, set, music: Music) => {
const { playlist } = get(this.atom);
if (playlist.some(({ id }) => id === music.id)) return;
set(this.atom, (prev: PlaylistStatus) => ({
...prev,
playlist: [...prev.playlist, music],
}));
}),
// ... other actions
};
// 4. Implement helper methods
private isEmpty(playlist: Music[]) {
return playlist.length === 0;
}
private isFirstMusic(index: number) {
return index === 0;
}
}
// 5. Create an instance of the Playlist class
const initialState: PlaylistStatus = {
playlist: [],
index: 0,
};
export const playlistManager = new Playlist(initialState);
- The
useManager
hook wraps the instance of theAtomManager
class and converts the abstracted selectors and actions intouseAtomValue
anduseSetAtom
hooks, respectively, inferring the types for the user.
// usePlaylist.ts
import { useManager } from '@pengoose/jotai';
import { playlistManager } from '@/viewModel';
export const usePlaylist = () => {
const {
selectors: { playlist, currentMusic },
actions: { play, next, prev, add, remove },
} = useManager(playlistManager); // 😀👍
return {
// Getters(Selectors)
playlist,
currentMusic,
// Setters(Actions)
play,
next,
prev,
add,
remove,
};
};
- You can use Jotai's
useAtomValue
anduseSetAtom
hooks to get and set the state of theAtomManager
instance without usinguseManager
. However, it is a bit cumbersome to use. 😨
// usePlaylist.ts
import { useAtomValue, useSetAtom } from 'jotai';
import { playlistManager } from '@/viewModel';
export const usePlaylist = () => {
const {
selectors: { playlist, currentMusic },
actions: { play, next, prev, add, remove },
} = playlistManager;
return {
// Getters(Selectors) 😨
playlist: useAtomValue(playlist),
currentMusic: useAtomValue(currentMusic),
// Setters(Actions) 😨
play: useSetAtom(play),
next: useSetAtom(next),
prev: useSetAtom(prev),
add: useSetAtom(add),
remove: useSetAtom(remove),
};
};
// Playlist.tsx
import { usePlaylist } from '@/hooks';
import { Music } from '@/types';
export const Playlist = () => {
const { playlist, currentMusic, play, next, prev, add, remove } =
usePlaylist();
return (
<div>
<h1>Playlist</h1>
<ul>
{playlist?.map((music) => {
const { id, title, thumbnail } = music;
return (
<li key={id}>
<img src={thumbnail} alt={title} />
<p>{title}</p>
<button onClick={() => remove(music)}>Remove</button>
</li>
);
})}
</ul>
<button onClick={() => add(currentMusic)}>Add to Playlist</button>
<button onClick={() => play(currentMusic)}>Play</button>
<button onClick={prev}>Prev</button>
<button onClick={next}>Next</button>
</div>
);
};
The AtomManager
class is designed to be used with custom hooks to encapsulate the state management logic and make it easier to use in your components.
Flow: Class(AtomManager) --> custom hook --> Component(View)
Contributions are welcome! For major changes, please open an issue first to discuss what you would like to change. ;)