@feinarbyte/atom-module
v8.4.0
Published
The Atoms Framework is a state management library designed to manage your application's state in a predictable way. It's based on the concept of "atoms", which are units of state that can be read and written to. Key Concepts
Downloads
85
Readme
Atoms Framework
The Atoms Framework is a state management library designed to manage your application's state in a predictable way. It's based on the concept of "atoms", which are units of state that can be read and written to. Key Concepts
Atoms: Atoms represent pieces of state in your application. They are the smallest units of state that can be read and written to. Atoms are defined in the backend and can be subscribed to in realtime using the useAtom hook.
Context: All atoms live in a certain context. Context is automatically provided in the frontend using the useContext hook. Context is automatically provided to all api calls and all subscriptions.
Reducers: The application is modified through reducers. Each reducers get the transaction as first argument and can specify an arbitrary number of other arguments.
Project Config: The Project Config is a configuration for the project that specifies the atoms, relations, and reducers used in the application.
Relations: Relations define how different atoms are related to each other.
Project Config
The Project Config is a configuration for the project that specifies the atoms, relations, and reducers used in the application.
export const AIConfig: ProjectConfig<AIAtoms, AIContext, AIRelations> = {
atomIndex: {
[AIAtoms.Todo]: TodoAtom,
// other atoms...
},
relations: {
[AIRelations.Todo]: {
identifier: AIContext.TodoId,
identifierType: 'string',
parents: [],
pathes(name) {
return [`t/${AIContext.TodoId}/${name}`];
},
reducers: [createTodo, toggleTodo, deleteTodo],
},
// other relations...
},
};
The ProjectConfig
is defined by the AIAtoms
, AIContext
, and AIRelations
enums. The AIAtoms
enum represents the atoms in the application, which are the smallest units of state that can be read and written to. The AIContext
enum represents the context in which all atoms live. This context is automatically provided in the frontend using the useContext
hook and is automatically provided to all API calls and all subscriptions. The AIRelations
enum defines are abstract entites like users. Each atom must belong to one of them. The entites can have hierarchical relations to each other, which are defined in the parents
property. The pathes
property defines the pathes in the database where the data for this relation is stored. The reducers
property defines the reducers that can be used to update the state of the atoms in this relation.
Defining Atoms
Atoms are defined as classes with the @ActiveAtom decorator. They represent a piece of state in your application.
export interface ITodo {
id: string;
text: string;
completed: boolean;
}
@ActiveAtom({
relation: AIRelations.Todo,
provideViaAtomResolver: (context, data): ITodo => {
return data;
},
})
export class TodoAtom extends Atom<AIAtoms.Todo, ITodo> implements ITodo {
public readonly __type = AIAtoms.Todo;
public id: string;
public text: string;
public completed: boolean;
}
Virtual Atoms
Virtual atoms are a special kind of atom that do not directly represent a piece of state, but instead derive their state from other atoms. They are defined as classes with the @ActiveAtom decorator, similar to regular atoms, but they extend the VirtualAtom class instead of the Atom class.
Here is an example of a virtual atom:
@ActiveAtom({
// Defines the relation for this virtual atom
relation: AIRelations.Tag,
// Resolver function to provide data for this virtual atom
provideViaAtomResolver: (context, data): ITag => {
return data;
},
})
// TagAtom is a virtual atom that derives its state from other atoms
export class TagAtom extends VirtualAtom<AIAtoms.Tag, ITag> implements ITag {
// The type of this atom, used for identification
public readonly __type = AIAtoms.Tag;
// The entries for this atom, derived from other atoms
public entries: { [entryId: string]: string };
// Dependencies of this virtual atom, used to derive its state
public static dependencies = [
{
// The EntryAtom dependency, from which this virtual atom derives its state
ctor: EntryAtom,
// Function to get context overrides from the EntryAtom dependency
getContextOverridesFromDependency: (data: IEntry) => {
return data.tags.map((tag) => ({ [AIContext.Tag]: tag }));
},
},
];
// Function to handle changes in dependencies
public async __onDependencyChange(
_query: string,
newValue: IEntry,
): Promise<void> {
const entryId = this.__getContextEntry(AIContext.EntryId);
this.entries[entryId] = newValue.title ?? (entryId as string);
}
// Function to handle deletion of entities
__onEntityDelete(relation: string, id: string): Promise<void> {
delete this.entries[id];
}
// Function to provide an empty value when no data is available
protected override __provideEmptyValue(): ITag {
return { entries: {} };
}
}
Defining Reducers in the backend
Reducers are functions that specify how the application's state changes in response to actions. They are used to handle and update the state of an atom.
export async function createTodo(
transaction: TransactionManager,
text: string,
): Promise<string> {
const todoId = Math.random().toString(36).substring(2, 15);
transaction.setEntry(AIContext.TodoId, todoId);
await transaction.spawnAtom(
TodoAtom,
{
id: todoId,
text,
completed: false,
},
{ [AIContext.TodoId]: todoId },
);
return todoId;
}
Accessing reducers in the frontend
there is a helper to create a hook that provides the api client to the frontend. The api client will group all reducers by relation and provide them as async functions that automatically trigger the reducer in the backend. The context is automatically passed to the reducer.
import { makeUseApiClient } from '@feinarbyte/atom-client';
import { APIClient } from '../generated/apiClient';
// Create a hook for the API client
export const useAiClient = makeUseApiClient(APIClient);
// Define a component that uses the API client
export const SomeComponent = () => {
// Get the API client
const aiClient = useAiClient();
// Return a div that creates a Todo when clicked
return <div onClick={() => aiClient.Todo.create('text')}>Click me</div>;
// ...
};
Reading Atoms in the frontend
Atoms can be subscribed to, this way all information will always be in sync with the backend automatically.
###creating a subscription client
import { makeUseAtom } from '@feinarbyte/atom-client';
import { atomDataIndex } from '../generated/atomdataindex';
import { AIAtoms } from '../generated/types';
export const useAiAtom = makeUseAtom<AIAtoms, atomDataIndex>();
using the subscription client
import { useAiAtom } from './useAiAtom';
// Define a functional component
export const SomeComponent = () => {
// Use the atom client to subscribe to the Tag atom
const tagAtom = useAiAtom(AIAtoms.Tag);
// Use the atom client to subscribe to the Tag atom with a specific context
const tagAtom = useAiAtom(AIAtoms.Tag, {[AiContext.Tag]: tagId});
// Use the atom client to subscribe to the Tag atom and extract the id
// If the tag is not found, it will return null
const tag = useAiAtom(AIAtoms.Tag, (tag) => tag.id) ?? null;
....