antvis / Graphin

🌌 A React toolkit for graph visualization based on G6.
https://github.com/antvis/graphin
MIT License
1.02k stars 266 forks source link

NEED FEEDBACK - Feature Idea -- Exportable GraphinProvider & useGraphin() react hook to access data from GraphinContext #333

Open cliffordfajardo opened 2 years ago

cliffordfajardo commented 2 years ago

Hi Graphin team & community, I would like your opinion on this feature proposal for the library.

Problem Statement

Currently, there is no way to access the GraphinContext data outside of the <Graphin> component. This is because @antv/graphin does not expose as an exportable GraphinProvider component.

Example / Scenario 1

Imagine I have a component called <NetworkTopologyViewer> which is responsible for setting up my <Graphin> component along with custom behavior

/**
* @description
* Renders a Graph wrapped in a custom container
* and also sets up custom node behavior
*/
const NetworkTopologyViewer = () => {
    const { graph } = React.useContext(GraphinContext); //❗ does not work. GraphinContext is not inside of a Provider
    const handleNodeClick = e => {
        console.log(e.item.getModel(); 
        makeAPICall(e.item.getModel()))
    }

    // ❗ Cannot setup custom node behavior here because `graph` cannot be grabbed from context above
    graph.on('node:click', handleNodeClick)

    return (
     <Card title="Network Topology">
        <Graphin {...GraphinProps....}>
                     <!- 😢 context data is only available in here. -->
        </Graphin>
      <Card>
  )
}

This is a live code example of what I'm trying to achieve above, which is to define a handleClick event for when a node in my graph gets clicked.



Proposal

Expose a useGraphin hook, which allows us get & set data from the GraphinContext.

// --------------------App.tsx--------
// Initialize with no data
import { GraphinProvider } from '@antv/graphin'
const graphinContextData = {...};

<GraphinProvider>
    <NetworkTopologyViewer />
</GraphinProvider>

// ---------OR initialize with data --------------
const graphinContextData = {...};
<GraphinProvider value={graphinContextData}>
   <NetworkTopologyViewer />
</GraphinProvider>

/**
* @description
* Renders a Graph wrapped in a custom container
* and also sets up custom node behavior
*/
const NetworkTopologyViewer = () => {
  const { graph, api} = useGraphin(); ✅ does not error because this component is inside a Provider in a parent component
  const handleNodeClick = e => {.....})

  graph.on('node:click', handleNodeClick) // I can setup my node behavior outside of <Graphin> 🎉

    return (
     <Card title="Network Topology">
        <Graphin {...GraphinProps....}/>
      <Card>
  )
}

Benefits

Steps required to implement useGraphin in @antv/graphin

  1. We would need to make GraphinProvider component & make it exportable to allow user's to wrap their component(s) with `GraphinProvider
    • By default if no data is passed into GraphinProvider the data will be empty, but after <Graphin> component loads, it will update the GraphinProvider's context data
      
      const graphinContextData = {...};

// ---------OR initialize with data -------------- const graphinContextData = {...};



## Example Fake Demo
Demo 1: https://stackblitz.com/edit/react-ts-cuhbx2?file=App.tsx
<img width="1231" alt="CleanShot 2021-12-05 at 17 50 22@2x" src="https://user-images.githubusercontent.com/6743796/144774762-32f52ed7-0960-41c3-a29b-3e69ce8a7cbd.png">

Demo2:
This demonstrates the hook pattern I'm describing (_to grab data from context data with hook_), which is  a common API pattern. This example show a `useAuth()` to demonstrate the concept described broadly.
   - https://stackblitz.com/edit/react-ts-p8dg9n?file=App.tsx
<img width="1628" alt="CleanShot 2021-12-05 at 17 55 56@2x" src="https://user-images.githubusercontent.com/6743796/144775155-48fce17c-7607-4b46-8ce8-90c79aee8f7c.png">

### References
This react pattern  of using a hook to get & set data from the context is often called the "_Action hooks pattern_". It's nice and flexible & quite popular in newer react libraries like `react-query`, `react-router` etc.
[Tanner Linsley](https://twitter.com/tannerlinsley) (creator of `react-query`) talking about this pattern:
   - https://www.youtube.com/watch?v=J-g9ZJha8FE&t=1s
   - https://www.youtube.com/watch?v=JRz-xMIyPUA

[Kent C Dodds](https://twitter.com/kentcdodds?ref_src=twsrc%5Egoogle%7Ctwcamp%5Eserp%7Ctwgr%5Eauthor) (creator or [react-testing-library](https://github.com/kentcdodds))
- https://kentcdodds.com/blog/authentication-in-react-applications
cliffordfajardo commented 2 years ago

@pomelo-nwu,@zxc0328


Hi @Blakko @timlrx @Anderson-Liu @budlion @niknbr @CodesAreHonest @peironggg

I've noticed some of you have contributed or mentioned using graphin so I've included many of you in this discussion.

Question(s)

  1. Do you have any opinions on this idea of a useGraphin() hook?

  2. Did any of you find the existing API limiting due to graphin not exposing something like a GraphinProvider component to wrap your component?

pomelo-nwu commented 2 years ago

@cliffordfajardo Thank you for your suggestions, we will think carefully and give some solutions. If you want, you can add my dingtalk and talk about it in detail sg

cliffordfajardo commented 2 years ago

Another Use Case Example

Below is another use case example of how having a useGraphin hook and a <GraphinProvider> is helpful. Imagine I want to place a search component above my <Graphin> component like this:

CleanShot 2021-12-10 at 10 33 05@2x

Currently this is not possible because the Graphin.Provider only exists inside of <Graphin> CleanShot 2021-12-10 at 10 36 04


UPDATE 12/12/2021

@pomelo-nwu - I've created a codesandbox demonstrating real usage of the useGraphin hook for the searching the graph example from the image above.

Summary of the example:

I was able to create a prototype of the desired behavior, without modifying graphin's internal code.

I had to create a wrapper component (see GrapinProvider.tsx inside the codesanbox link below) I would say it is a good non-hacky abstraction. Ideally we wouldn't need the wrapper component, but it could serve lots of people well in the meantime before it makes it into the core Graphin library/

If the proposed code changes for useGraphin hook does make it into the @antv/graphin library one day, people using the example abstraction (GraphnProvider.tsx) I provided in the code sandbox wouldn't need to change almost anything when the GraphinProvider and useGraphin is exposed from the real library

Concerning what you mentioned on Graphin's DingTalk channel:

The reason why we didn't disclose the GraphinProvider to users was that when we designed it at that time, for the out-of-the-box use of components, for many developers, we added a layer of Provider writing, it may make them feel that the threshold for getting started is very high. At the same time, we generally use React.createPortal to solve the hierarchy problem of components, to ensure that all components are placed inside

I believe it should still be possible to keep the existing behavior of passing "behaviors" inside the component to make it easy for developer's to get started with.

Ideally though the way to make this work in the real @antv/graphin library is to convert the <Graphin> component to a Functional component instead of it remaining a Class component.

Tips for navigating the codesanbox:

Note:

My Future Plans

To prevent destructive upgrades my plan is to:

mxz96102 commented 2 years ago

Hi, I'm also interested in 'useGraphin' hook. Features such as isReady, APIs and etc. are so effective.

But in my opinion, it should be done with fewer side effects for graphin. So I came up with an idea that uses a global "Graphin pool" for Graphin hooks.

For users who wants to useGraphin, they only need to append a global unique id to Graphin component, like \<Graphin id="graph-1" \/> and it can useGraphin("graph-1") anywhere.

Instance of Graphin will register into Graphin pool when init and remove when destroy, with register event, context will directly avaliable for useGraphin. (It's like a pub/sub model), and for users, they don't need to know graphin context, only change they need to know is that they can assign id to graphin instance and use graphin api throgh id anywhere they need.

What's your opinion about this pattern, looking forwards to your reply~

cliffordfajardo commented 2 years ago

Hi @mxz96102 - I will share some thought's later today exploring concerning your idea.

Features such as isReady, APIs and etc. are so effective.

I agree!

timlrx commented 2 years ago

It's a nice idea! Currently for motif, we are just storing the ref from Graphin and passing it through to the other widget components using a provider. The other components can then useContext and have access to the full G6 api.

A useGraphin would help simplify things.

cliffordfajardo commented 2 years ago

Hi @mxz96102

But in my opinion, it should be done with fewer side effects for graphin

I agree that the changes that are made to Graphin should not dramatically change the usage of the library. @mxz96102 would you be able to share an example of what the code looks like in practice via a codesanbox, some visual or some pseudo code? I just want to make sure I understand your idea 😄

For this to Graphin Pool idea to work we would probably would need to write some code inside the useGraphin hook that searches the DOM for the element on the page with id="graph-1".

Thanks for bringing up the idea of multiple <Graphin> components on the same page. This is a real use case & I'm actually doing this at work for page I built 🙂

With all that said, if we converted the <Graphin> from a Class component to a Function component & created the GraphinProvider.tsx file we wouldn't need a Graphin pool & globals (this makes testing harder) & we wouldn't run into the tricky scenarios I mentioned before.

Example of multiple Graphin components on a page below:

const Page = () => {
    return (
        <>

        <GraphinProvider>
                useGraphin() here will have access to the GraphinProvider's context, which is connected to the Graphin Component  be low it
            <Graphin></Graphin>            
        </GraphinProvider>    

        <GraphinProvider>
                 useGraphin() here will grab data from the GraphinContext inside the GraphinProvider that is directly above it only (this is normal out of the box react behavior)

            <Graphin></Graphin>            
        </GraphinProvider>            

        </>
    )
}

Other thoughts

For the longevity of the Graphin project, I do believe its in the best interest of the library & it's community to migrate away from having the Graphin.tsx file be a Class component and to rather have it be a Function component for a few reasons:

@mxz96102 I liked the concept of the global pool 💯 , but the technical implementation & limitations it brings in the near future I think are not worth it compared to the approach of having an exportable <GraphinProvider> which is the more idiomatic React approach

Summary

This approach of creating an exportable GraphinProvider in order to help us achieve the useGraphin hook would still allow you to pass in behavior components inside <Graphin>

  <Graphin
    data={graphData}
    layout={{ type: ".." }}
  >
    // you would still be able to pass in Behaviors with the new proposed changes (backwards compatible)
    <SetupUseGraphinHook />
    <ZoomCanvas />
  </Graphin>

Updated smaller codesandbox of using useGraphin with multiple Graph's on the page

mxz96102 commented 2 years ago

@cliffordfajardo I came up with an id sample here and just extend the component, hope can help you in someway.

https://codesandbox.io/s/hookedgraphin-6bbsr?file=/App.tsx

cliffordfajardo commented 2 years ago

@mxz96102 - can you share more about your thoughts on the advantages / disadvantages of this approach versus other alternatives we've talked about in this discussion?

Genuinely, I am just curious and would like understand your idea better 🙏

Some high level thoughts on the Graphin Pool Approach

With the Graphin pool approach, we are introducing a new concept of a pool, and having to manage that pool; I know its not much, but with the GraphinProvider approach I've discussed above, we are not introducing new concepts into the codebase. Its really about 2 key things:

All the existing behavior will still just work 😄

Sorry if I wasn't clear, next time I will make a much smaller codesanbox like you

mxz96102 commented 2 years ago

I agree that deprecate class component will embrace more features of React 18, but for now, it might not be done in this version, considering that some of the users still depend on class lifecycle to do something, in this version using id might be enough for users, and for break change (like converting Graphin to function component), may happen in a new version like Graphin 3?