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,
},
relations: {
[AIRelations.Todo]: {
identifier: AIContext.TodoId,
identifierType: 'string',
parents: [],
pathes(name) {
return [`t/${AIContext.TodoId}/${name}`];
},
reducers: [createTodo, toggleTodo, deleteTodo],
},
},
};
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({
relation: AIRelations.Tag,
provideViaAtomResolver: (context, data): ITag => {
return data;
},
})
export class TagAtom extends VirtualAtom<AIAtoms.Tag, ITag> implements ITag {
public readonly __type = AIAtoms.Tag;
public entries: { [entryId: string]: string };
public static dependencies = [
{
ctor: EntryAtom,
getContextOverridesFromDependency: (data: IEntry) => {
return data.tags.map((tag) => ({ [AIContext.Tag]: tag }));
},
},
];
public async __onDependencyChange(
_query: string,
newValue: IEntry,
): Promise<void> {
const entryId = this.__getContextEntry(AIContext.EntryId);
this.entries[entryId] = newValue.title ?? (entryId as string);
}
__onEntityDelete(relation: string, id: string): Promise<void> {
delete this.entries[id];
}
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';
export const useAiClient = makeUseApiClient(APIClient);
export const SomeComponent = () => {
const aiClient = useAiClient();
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';
export const SomeComponent = () => {
const tagAtom = useAiAtom(AIAtoms.Tag);
const tagAtom = useAiAtom(AIAtoms.Tag, {[AiContext.Tag]: tagId});
const tag = useAiAtom(AIAtoms.Tag, (tag) => tag.id) ?? null;
....