Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP feat(joint-react): react lib project setup + POC's #2867

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from

Conversation

samuelgja
Copy link

@samuelgja samuelgja commented Jan 31, 2025

Description

WIP react lib project setup + POC's

Public API

  1. GraphProvider component - Provides dia.Graph to the React context, accessible anywhere in its children.
  2. PaperProvider component - Provides dia.Paper to the React context. It's optional; if not used, the Paper component will create its own Paper instance.
  3. Paper component - Provides a view (UI) for the graph elements.

The user can utilize built-in JointJS shapes or custom React components.

Two proposed APIs how it should "react" with react state:

  1. Using React state for elements (cells) with the ability to still use the dia.Graph API:

    // React component
    const [cells, setCells] = useState([])
    return <GraphProvider cells={cells}>...</GraphProvider>
  2. Using the dia.Graph imperative API as the source of truth and custom hooks for React reactivity for elements (cells) + some sugar utilities for editing updating:

    const graph = new dia.Graph(...)
    
    // React component
    return <GraphProvider graph={graph}>...</GraphProvider>

In my opinion, the second approach is clearer and better for performance - as it use react state and react re-renders only if user really need it. It leverages the dia.Graph imperative API rather than the React declarative way, which is not too much react way, but synchronizing the graph with React state can be challenging and may impact performance. This approach works well within the JointJS space, avoiding React states.

When users want to interact with elements or cells via React components, they can use the hooks API, such as useGraphCells which will synchronize jointjs elements state with react state in bi-directional way (view / edit)

For example, to control (add / remove/ edit) or display particular graph elements inside a React component:

function SomeController() {
  // This must be used inside GraphProvider
  const [cells, setCells] = useGraphCells()
  return <div onClick={() => setCells([])}>{JSON.stringify(cells)}</div>
}

Motivation and Context

This change is required to introduce the new joint-react package, providing a more efficient and clear way to manage graph elements in React applications.

More info

To test this out, just run yarn storybook in newly created package packages/joint-react

This is just POC, it do not use any external UI library or something and I think it should not, for more UI examples we should add them inside example folder.

In this project, I will try to focus to these points all the time:

  1. easy to use - simple and understandable API
  2. performance - make sure to avoid unesesery re-renders or memory leaks
  3. compatible with other react UI libraries (tailwind or other full UI libs)
  4. easy to migrate for existing joint js users

So if there is not something clear or something do not make sense, let me know in the comments.

Screenshots (if appropriate):

This is the first PR for this project.

@samuelgja samuelgja force-pushed the feat/joint-react-with-portals branch from 7c00f5f to ef39d8d Compare February 5, 2025 07:51
@samuelgja
Copy link
Author

samuelgja commented Feb 5, 2025

PR Update: API Improvements (POC v2)

Following discussions with @kumilingus, the API has been refined to be cleaner and more user-friendly.

Changes & Structure

The components remain largely the same:

  • GraphProvider: Holds the dia.Graph instance inside a React context.
  • PaperProvider (optional): Holds the dia.Paper instance inside a React context. If not provided, the Paper component will create its own instance of dia.Paper.
  • Paper component: Assigns a paper to an HTML element and renders elements in a React-friendly manner.

Public Hooks

The following hooks are available:

  • usePaper hook: The Paper component internally uses usePaper, which can also be used publicly by other users if needed. It returns only a ref to be assigned to a div and supports the same options as Paper, plus an additional onRenderMethod that provides the assigned HTML element (if available).
  • useSetGraphCells – Used to set graph cells.
  • useElements – Returns elements.
  • useLinks – Returns links.

Both useElements and useLinks accept an optional selector parameter to extract specific data from nodes or edges. However, this needs testing for React re-renders, as currently, it seems not 100% optional—it creates a new instance on every render - but I know already how to fix this.

Example:

useElements(element => ({ id: element.id }));

➡️ We need to verify that this does not cause unnecessary re-renders.

Internal Mechanics

  • GraphProvider creates graphStore, which subscribes to graph changes and tracks element and link information.
  • useElements and useLinks leverage useSyncExternalStore react API, which optimizes performance by reducing unnecessary re-renders and avoiding data duplication.

I am still exploring more ways how to do this - but this way seems to be flexible and easy to use - I am not big fun of contexts, so if there are some concerns about it, I am open to discuss.

EDIT:
useElements and useLinks have two options now:

  1. selector function which default to default element or default link
  2. comparison function which default to shallow equality check function

Screenshots of POC

Get data from react and update it from react (we can get and update any data between react and jointjs):
https://github.com/user-attachments/assets/205a3ad2-a718-4ee2-9a25-943eca32821d

Simple stress test - to render 450 elements with links between each
https://github.com/user-attachments/assets/aa5594f3-a0bc-4ed9-95df-718d2ea001d9

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants