Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork137
Description
This is not an issue, rather a comment with examples.
Hooks have been around in React for a few releases now and they can make integration with 3rd party DOM libraries much easier.
As an example, integrating Plotly can be as simple as this:
// global: Plotlyimport{useState,useLayoutEffect}from'react';functionusePlotlyBasic({ data, layout, config}){const[ref,setRef]=useState(null);constuseLayoutEffect(()=>{ref&&Plotly.react(ref,{ data, layout, config})return()=>{ref&&Plotly.purge(ref)};},[ref,data,layout,config])returnsetRef;}
It exposes acallback ref which is used like so:
functionMyPlot(props){constref=usePlotlyBasic(props)return<divref={ref}/>}
This tiny function gives a lot of the functionality of react-plotly. Like react-plotly, it is 'dumb' in that you must update the props (layout, config, state) for it to update (and that update must be immutable).
Since all rendering of the parent<div>
is handed off to the caller, there is no need to accept props such asdivId
,className
,style
. In doing so, the code is both simplified and less restrictive to the user.
Immutable updates can be a pain to manage, but we can also offload that to the hook. In the next example, I am making use ofstreams (from the excellent flyd library, but you could use rxjs, most, xstream, kefir etc...) so that the user can just pass partial updates:
// global: Plotlyimport{useState,useLayoutEffect}from'react';import{stream,scan}from'flyd'import{mergeDeepRight}from'ramda';exportdefaultfunctionusePlotly(){constupdates=stream();constplotlyState=scan(mergeDeepRight,{data:[],config:{},layout:{}},updates);const[internalRef,setRef]=useState(null);useLayoutEffect(()=>{if(internalRef){constendS=plotlyState.map(state=>{Plotly.react(internalRef,state);});return()=>{Plotly.purge(internalRef);endS.end(true);};}},[internalRef]);return{ref:setRef, updates};}
Here, you can just push partial updates on to theupdates
stream:
constrandomData=()=>Array.from({length:10},Math.random)functionMyPlot(props){const{ ref, updates}=usePlotly(props)constonClick=()=>updates({data:{y:randomData(),type:'scatter'}});return<div><button>Plot!</button><divref={ref}/></div>}
I am usingmergeDeepRight
fromramdajs
to merge the previous state with the partial update, but you could use your own function (e.g. based on immerJS for example, or using JSON Patch).
A more complex example. Plotly's/react-plotly's responsive behaviour has always bugged me. Theresponsive
property only responds to window resize events. I often want to allow users to resize the individual element holding the chart. Using hooks, streams and the ResizeObserver API, this is trivial:
// global: Plotly, debounceimport{useLayoutEffect,useState,useCallback}from'react';import{head,prop,compose,pick,objOf,mergeDeepRight}from'ramda';import{stream,scan}from'flyd';constgetSizeForLayout=compose(objOf('layout'),pick(['width','height']),prop('contentRect'),head);exportdefaultfunctionusePlotly(){constupdates=stream();constplotlyState=scan(mergeDeepRight,{data:[],config:{},layout:{}},updates);constobserver=newResizeObserver(debounce(compose(updates,getSizeForLayout),100));const[internalRef,setRef]=useState(null);useLayoutEffect(()=>{if(internalRef){observer.observe(internalRef);constendS=plotlyState.map(state=>{Plotly.react(internalRef,state);});return()=>{Plotly.purge(internalRef);observer.unobserve(internalRef);endS.end(true);};}},[internalRef]);return{ref:setRef, updates};}
Here, I used ramdajs to extract the new size information from the ResizeObserver callback, but it could be written in a more imperative style as:
functiongetSizeForLayout(entries){const{ width, height}=entries[0].contentRect;return{layout:{ width, height}};}
I also included a debounce function (easy to implement natively or take from e.g. lodash) to prevent to many redraws when you are actively dragging to resize.
OK, final example. With hooks, it becomes trivial to expose other parts of the plotly API. Here I include all of the previous examples and add a stream which will take data updates to pass directly toPlotly.extendTraces
:
// global: Plotly, debounceimport{useLayoutEffect,useState}from'react';import{head,prop,compose,pick,objOf,mergeDeepRight}from'ramda';import{stream,scan}from'flyd';constgetSizeForLayout=compose(objOf('layout'),pick(['width','height']),prop('contentRect'),head);exportdefaultfunctionusePlotly(){constupdates=stream();constappendData=stream();constplotlyState=scan(mergeDeepRight,{data:[],config:{},layout:{}},updates);constobserver=newResizeObserver(debounce(compose(updates,getSizeForLayout),100));const[internalRef,setRef]=useState(null);useLayoutEffect(()=>{if(internalRef){observer.observe(internalRef);constendS=plotlyState.map(state=>{Plotly.react(internalRef,state);});constendAppend=appendData.map(({ data, tracePos})=>Plotly.extendTraces(internalRef,data,tracePos));return()=>{Plotly.purge(internalRef);observer.unobserve(internalRef);endAppend.end(true);endS.end(true);};}},[internalRef]);return{ref:setRef, updates, appendData};}
Overall, my point is that I think hooks allow you to provide a smaller, more flexible and easy to use API. I would love to see ausePlotly
hook (hopefully incorporating some of the ideas above as options) as an alternative to the<Plot>
widget.