DEV Community

Cover image for Achieve fine grained reactivity and super fast UI updates in React with Legend-state
Harsh Mangalam
Harsh Mangalam

Posted on

Achieve fine grained reactivity and super fast UI updates in React with Legend-state

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

Enter fullscreen mode Exit fullscreen mode

Setup tailwindcss

pnpm i -D tailwindcss postcss autoprefixer
pnpx tailwindcss init -p

Enter fullscreen mode Exit fullscreen mode

tailwind.config.js

import daisyui from "daisyui"
/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

Enter fullscreen mode Exit fullscreen mode

src/index.css

@tailwind base;
@tailwind components;
@tailwind utilities;

Enter fullscreen mode Exit fullscreen mode

Add daisyui plugins to make our component beautiful with fewer classes.

npm i -D daisyui@latest

Enter fullscreen mode Exit fullscreen mode

tailwind.config.js


module.exports = {
  //...
  plugins: [require("daisyui")],
}

Enter fullscreen mode Exit fullscreen mode

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;
};

Enter fullscreen mode Exit fullscreen mode

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: [],
});

Enter fullscreen mode Exit fullscreen mode

Update main.tsx


import { enableReactUse } from "@legendapp/state/config/enableReactUse";

enableReactUse(); // This adds the use() function to observables

// ...
// ...


Enter fullscreen mode Exit fullscreen mode

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>
  );
}


Enter fullscreen mode Exit fullscreen mode

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>
  );
}


Enter fullscreen mode Exit fullscreen mode

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>
  );
}


Enter fullscreen mode Exit fullscreen mode

You can go through legend-state documentation here
Full source code is available on github

GitHub logo 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:

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,
   },
Enter fullscreen mode Exit fullscreen mode
  • Replace plugin:@typescript-eslint/recommended to plugin:@typescript-eslint/recommended-type-checked or plugin:@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 the extends list

Also i have developed for nextjs using nextjs-13 app router | shadcn-ui | legend-state

GitHub logo 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
Enter fullscreen mode Exit fullscreen mode

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:

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)

Collapse
 
syedsheharyar profile image
Syed-Sheharyar

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.

Collapse
 
harshmangalam profile image
Harsh Mangalam

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.

Collapse
 
golam_mostafa profile image
Golam_Mostafa

Great!. Will you share the code of local persistence using legend state plugin?