Introduction
There are many libs for state management. But Elf (https://ngneat.github.io/elf/) have a good point than other.
🤘 It's independent, no bind to FW, you can use everywhere. Share the same class to many projects 😍
Here we are going to see how to create todo app with setup all logic into 1 file (elf repository)
Demo
Source code : https://github.com/workfel/react-todo-elf
Demo : https://workfel.github.io/react-todo-elf/
Installation
Use
create-react-appand tailwindcss. https://tailwindcss.com/docs/guides/create-react-appAdd
elflib
npm i —save @ngneat/elf @ngneat/elf-entitiesAdd
npm i --save @ngneat/use-observableto use custom hook with Observable .
Structure
Create folder into src
- components ("dumb")
- containers ("smart" components)
- infrastructures (manage context app)
- repository (todo repo)
Repository
Start by creating skeleton of repository into repository/todo.repository.ts. Will be responsible for the logic of app.
// datas models
// repository/todo.repository.ts
export interface Todo {
id: string;
name: string;
completed: boolean;
}
export interface VisibilityFilterProps {
filter: 'active' | 'completed' | 'all';
}
We describe all we can do with the app.
// repository/todo.repository.ts
export interface TodoRepository {
todos$: Observable<Todo[]>;
addTodo(text: Todo['name']): void;
markAsComplete(id: string): void;
removeTodo(id: string): void;
markAsActive(id: string): void;
updateFilter(type: VisibilityFilterProps['filter']): void;
}
Now create the state of app. Contains list of Todo with withEntities<Todo>() and props filter to manage the items to display withProps<VisibilityFilterProps>({ filter: 'all' })
// repository/todo.repository.ts
const { state, config } = createState(
withProps<VisibilityFilterProps>({ filter: 'all' }),
withEntities<Todo>(),
);
Create store into repo class
We will name todos the name of store and pass state & config previously created.
// repository/todo.repository.ts
export class TodoRepositoryElf implements TodoRepository {
private todosStore = new Store({ name: 'todos', state, config });
todos$: Observable<Todo[]>;
addTodo(text: Todo['name']): void {
}
markAsActive(id: string): void {
}
markAsComplete(id: string): void {
}
removeTodo(id: string): void {
}
updateFilter(type: VisibilityFilterProps['filter']): void {
}
}
Add context
Create context TodoContext will permit to access on repository everywhere on app by using hook useContext
// infrastructure/todo.context.provider.ts
export interface TodoContextInterface {
repository: TodoRepository;
}
export const TodoContext = createContext<TodoContextInterface>({
repository: {} as TodoRepository
});
export default TodoContext;
Add context around <App/>. And set the repository to use on TodoContext.Provider
// index.tsx
ReactDOM.render(
<React.StrictMode>
<TodoContext.Provider value={{ repository: new TodoRepositoryElf() }}>
<App/>
</TodoContext.Provider>
</React.StrictMode>,
document.getElementById('root')
);
React side
Todo
Todo component will contain all components of app, and communicate with repository via the context TodoContext created before.
// containers/Todo.tsx
const Todo = () => {
// get repository from context
const { repository } = useContext(TodoContext);
// Call repo when visibility filter has changed
const handleVisibility = ((filter: VisibilityFilterProps['filter']) => {
repository.updateFilter(filter);
});
// Call repo when new todo is added
const handleAddTodo = ((text: string) => {
repository.addTodo(text);
});
return <div className="container mx-auto p-8 flex flex-col grow h-full">
<div className="flex flex-col grow">
<h1 className="text-4xl font-semibold mt-8 mb-8">
All tasks
</h1>
<h2 className="font-semibold uppercase text-xl tracking-wide text-slate-400 mt-8 mb-4">
Filters
</h2>
<VisibilityFilter onChange={handleVisibility}/>
<TodoList/>
</div>
<div>
<AddTodo onAdd={handleAddTodo}/>
</div>
</div>;
};
export default Todo;
TodoList
This component list all todo items. And manage state of todo by complete/active/remove TodoItem.
With useObservable when todos$ is updated on repository the value todos will be automatically updated.
// containers/TodoList.tsx
const TodoList = () => {
const { repository } = useContext(TodoContext);
const [todos] = useObservable(repository.todos$);
// Call repo to mark item completed
const handlerComplete = (id: string) => {
repository.markAsComplete(id);
};
// Call repo to mark item active
const handleActive = (id: string) => {
repository.markAsActive(id);
};
// Call repo to remove item
const handleRemove = (id: string) => {
repository.removeTodo(id);
};
return (
<div>
<h2 className="font-semibold uppercase text-xl tracking-wide text-slate-400 mt-8 mb-4">Todo List</h2>
{todos.map((todo) => (
<TodoItem todo={todo} onCompleted={handlerComplete} onRemove={handleRemove}
onActive={handleActive}
key={todo.id}/>
))}
</div>
);
};
export default TodoList;
VisibilityFilter
"Dumb" component just raised event when filter value change.
//components/VisibilityFilter.tsx
const VisibilityFilter = ({ onChange }: { onChange: (filter: VisibilityFilterProps['filter']) => void }) => {
return (
<div className="flex gap-x-2 justify-center">
<ButtonFilter onClick={onChange} id="all">
All
</ButtonFilter>
<ButtonFilter onClick={onChange} id="active">
Active
</ButtonFilter>
<ButtonFilter onClick={onChange} id="completed">
Completed
</ButtonFilter>
</div>
);
};
export default VisibilityFilter;
AddTodo
Simple form with input and button who raised event onAdd with input value when the "add" button is clicked
// components/AddTodo.tsx
const AddTodo = ({ onAdd }: { onAdd: (name: string) => void }) => {
const inputRef = useRef<HTMLInputElement>(null);
const submitHandler = (e: React.FormEvent) => {
e.preventDefault();
const todoValue = inputRef.current?.value as string;
inputRef.current!.value = '';
if (todoValue) {
onAdd(todoValue);
}
};
return (
<div className="">
<form className="flex mt-4" onSubmit={submitHandler}>
<input className="shadow appearance-none border rounded w-full py-2 px-3 mr-4 text-slate-900"
placeholder="Add Todo"
ref={inputRef}/>
<button
className="flex-no-shrink p-2 border-2 rounded-full border-green-500 bg-green-500 hover:text-white hover:bg-green-600 fill-white hover:fill-green-300"
type="submit">
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
width="24" height="24"
viewBox="0 0 24 24">
<path fill-rule="evenodd"
d="M 11 2 L 11 11 L 2 11 L 2 13 L 11 13 L 11 22 L 13 22 L 13 13 L 22 13 L 22 11 L 13 11 L 13 2 Z"></path>
</svg>
</button>
</form>
</div>
);
};
export default AddTodo;
The Single Source Of Truth
Now we have plugged all UI event with the repository, but the repository do nothing at this time, so we are going to do this.
List
First we are to setup the todos$. The list of items will be all entities filtered by the props filter. When all is applied all todos will be listed, completed only the todos with completed:true will be listed , and active only the completed:false.
First we get the value of filter, on the repo
// repository/todo.repository.ts
export class TodoRepositoryElf implements TodoRepository {
private todosStore = new Store({ name: 'todos', state, config });
filter$ = this.todosStore.pipe(select(({ filter }) => filter));
//....
}
Now we have the filter value , we are set the todos$ observable.
// repository/todo.repository.ts
//....
todos$: Observable<Todo[]> = this.filter$.pipe(switchMap((filter) => {
return this.todosStore.pipe(selectAllApply({
filterEntity({ completed }): boolean {
if (filter === 'all') return true;
return filter === 'completed' ? completed : !completed;
}
}));
}));
//....
See in action by implementing addTodo. To add entities simply use addEntities https://ngneat.github.io/elf/docs/features/entities/entities#addentities
// repository/todo.repository.ts
addTodo(text: Todo['name']) {
this.todosStore.update(addEntities({
name: text,
id: Date.now().toString(),
completed: false
}));
}
Good now implements all methods
markAsComplete(id: string) {
this.todosStore.update(updateEntities(id, {
completed: true
}));
}
markAsActive(id: string) {
this.todosStore.update(updateEntities(id, {
completed: false
}));
}
removeTodo(id: string): void {
this.todosStore.update(deleteEntities(id));
}
updateFilter(type: VisibilityFilterProps['filter']): void {
this.todosStore.update((state) => ({
...state,
filter: type,
}));
}
And
voilà.
Conclusion
Using Elf it's really easy and simple. You even can use the todo.repository.ts in your Angular Vue Svelte app because it's independent to FW.
Source : https://github.com/workfel/react-todo-elf
Demo : https://workfel.github.io/react-todo-elf/

Top comments (0)