Introduction
Legend state is a super fast state management library that is built on top of observables.
Features
- Better Performance
- Easy to use
- Fine grained reactivity
- Built-in persistent
- Better memory usage
- No Boilerplate
- Lightweight (~4kb)
- Better DX (Developer Experience)
Legend-state Observables use Proxy in a unique way which is really fast and it doesn't modify the underlying data.
In optimized mode for an array only re render the changed element instead of the whole array.
Legend-state render component only once and update needed part of ui yes it feels like the Solid.js.
If we look into the current mental model of React when we update the state it rerender the component and then execute side effects.
State changed => Rerender component => Execute side effect
But legend mental model skip the second step from React mental model and execute your side effect directly after state change.
State changed => Execute side effect
If it comes to optimize react app we jump into applying useCallback
, useMemo
to prevent from unusual rerender but in legend component render only once you do not required these react hooks.
You can easily replace useState
and useReducer
with useObservable
hook that keep track of deep object and notify listeners when state change and instead of rerendering the whole component it will only updates the needed part of UI.
In the same way you can replace useEffect
with useObserve
so no need to maintain the dependency array.
Okay now we are going to develop a todo app that will utilize legend-state and natural fine grained reactivity.
Initialize new project with vite react-ts and follow the terminal instructions.
pnpm create vite todo --template react-ts
Setup tailwindcss
pnpm i -D tailwindcss postcss autoprefixer
pnpx tailwindcss init -p
tailwind.config.js
import daisyui from "daisyui"
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
src/index.css
@tailwind base;
@tailwind components;
@tailwind utilities;
Add daisyui plugins to make our component beautiful with fewer classes.
npm i -D daisyui@latest
tailwind.config.js
module.exports = {
//...
plugins: [require("daisyui")],
}
Define Todo type and Status enum
types/todo.ts
export enum Status {
Initialized = "initialized",
Progress = "progress",
Done = "done",
}
export type Todo = {
id: string;
text: string;
status: Status;
};
Create global store using legend state observable
src/store/todo.ts
import { observable } from "@legendapp/state";
import { Todo } from "../types/todo";
export const state = observable<{ todos: Todo[] }>({
todos: [],
});
Update main.tsx
import { enableReactUse } from "@legendapp/state/config/enableReactUse";
enableReactUse(); // This adds the use() function to observables
// ...
// ...
Update App.tsx
import TodoItem from "./components/todo-item";
import { state } from "./store/todo";
import { Status } from "./types/todo";
import { For, Reactive, useObservable } from "@legendapp/state/react";
import { enableReactComponents } from "@legendapp/state/config/enableReactComponents";
enableReactComponents();
export default function App() {
const input = useObservable("");
const handleAddTodo = () => {
state.todos.push({
id: crypto.randomUUID(),
status: Status.Initialized,
text: input.get(),
});
input.set("");
};
return (
<div className="min-h-screen h-full bg-base-100 py-8">
<div className="max-w-xl mx-auto">
<section className="flex flex-col gap-y-2 items-center text-center">
<h1 className="font-bold text-3xl">Todo</h1>
<p className="text-xl">
Todo web app build with React | Vite | Legend State | Tailwindcss |
Daisyui
</p>
</section>
<section className="mt-8 flex items-center gap-2">
<Reactive.input
autoFocus
placeholder="Start typing..."
$value={input}
className="input input-bordered w-full"
/>
<button onClick={handleAddTodo} className="btn btn-primary">
Add
</button>
</section>
<section className="mt-8">
<div className="grid grid-cols-1 gap-2">
<For each={state.todos}>
{(todo) => <TodoItem todo={todo.get()} />}
</For>
</div>
</section>
</div>
</div>
);
}
Legend-State provides reactive versions of all platform components with reactive props to use these components first enable it using enableReactComponents()
Reactive.input
add two-way binding to the value, so that the observable is always in sync with the displayed value of the element.
<For>
component is optimized for rendering arrays of observable objects so that they are extracted into a separate tracking context and don't re-render the parent.
Using .get()
we can get value from Observable and .set()
trigger the state update.
Create src/components/todo-item.tsx
import { state } from "../store/todo";
import { Status, Todo } from "../types/todo";
import { Reactive } from "@legendapp/state/react";
import TodoStatus from "./todo-status";
export default function TodoItem({ todo }: { todo: Todo | undefined }) {
const handleRemoveTodo = () => {
state.todos.set((todos) => todos.filter((t) => t.id !== todo?.id));
};
return (
<Reactive.article
$className={
todo?.status === Status.Progress
? "border-warning card card-bordered"
: todo?.status === Status.Done
? "border-error card card-bordered"
: "card card-bordered"
}
>
<div className="card-body p-4">
{todo?.text}
<div className="card-actions flex gap-2">
<TodoStatus id={todo?.id} status={todo?.status} />
<button onClick={handleRemoveTodo} className="btn btn-error btn-sm">
Remove
</button>
</div>
</div>
</Reactive.article>
);
}
src/components/todo-status.tsx
import { state } from "../store/todo";
import { Status } from "../types/todo";
export default function TodoStatus({
id,
status,
}: {
id: string | undefined;
status: Status | undefined;
}) {
const handleChangeStatus = (status: Status) => {
state.todos.set((todos) =>
todos.map((todo) => {
if (todo.id === id) {
return {
...todo,
status,
};
}
return todo;
})
);
};
return (
<select
className="select flex-1 select-sm select-bordered"
value={status}
onChange={(e) => handleChangeStatus(e.target.value as Status)}
>
<option value={Status.Initialized}>Initialized</option>
<option value={Status.Progress}>Progress</option>
<option value={Status.Done}>Done</option>
</select>
);
}
You can go through legend-state documentation here
Full source code is available on github
harshmangalam / reactjs-legend-state-daisyui-todo
Todo web app build with React 18 | Vite | Tailwindcss | Dasiyui | Legend state
React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- @vitejs/plugin-react uses Babel for Fast Refresh
- @vitejs/plugin-react-swc uses SWC for Fast Refresh
Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level
parserOptions
property like this:
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
- Replace
plugin:@typescript-eslint/recommended
toplugin:@typescript-eslint/recommended-type-checked
orplugin:@typescript-eslint/strict-type-checked
- Optionally add
plugin:@typescript-eslint/stylistic-type-checked
- Install eslint-plugin-react and add
plugin:react/recommended
&plugin:react/jsx-runtime
to theextends
list
Also i have developed for nextjs using nextjs-13 app router | shadcn-ui | legend-state
harshmangalam / nextjs-legend-state-shadcn-ui-todo
Todo web app build with Next.js 13 app router | Legend state | Shadcn-ui
This is a Next.js project bootstrapped with create-next-app
.
Getting Started
First, run the development server:
npm run dev
# or
yarn dev
# or
pnpm dev
Open http://localhost:3000 with your browser to see the result.
You can start editing the page by modifying app/page.tsx
. The page auto-updates as you edit the file.
This project uses next/font
to automatically optimize and load Inter, a custom Google Font.
Learn More
To learn more about Next.js, take a look at the following resources:
- Next.js Documentation - learn about Next.js features and API.
- Learn Next.js - an interactive Next.js tutorial.
You can check out the Next.js GitHub repository - your feedback and contributions are welcome!
Deploy on Vercel
The easiest way to deploy your Next.js app is to use the Vercel Platform from the creators of Next.js.
Check out our Next.js deployment documentation for more details.
Top comments (3)
Much recommended don't re-invent the wheel and enjoy all the nice things of Solid JS reactivity in React. It's Legend state for you. Also, it is production ready.
I have played too much with solid.js and really i can connect with this fact . Legend State mental modal removed the burden of performance optimization and prevent from unusual rerender.
Great!. Will you share the code of local persistence using legend state plugin?