diff --git a/README.md b/README.md
index e45df6e..b6c6613 100644
--- a/README.md
+++ b/README.md
@@ -113,6 +113,34 @@ In short, this means that simply adding data points to a trace in `data` or chan
## API Reference
+### usePlotly Hook
+
+As an alternative to the `Plot` component, you may use the `usePlotly` react _hook_. This provides a more powerful API with full control over the plot element, compatibility with functional components, intuitive responsive behaviour and ability to use `extendTraces`.
+Here is a simple example of creating a chart with `usePlotly`:
+
+```jsx
+function MyChart(props) {
+ const { ref, updates, appendData } = usePlotly();
+
+ // Here is a function that will change the data. You must pass a partial Figure object (plotly DSL object) which will be
+ // merged with all previous calls to `updates`
+ const changeData = () => updates({ data: [ { y: [Math.random() * 10], type: 'scatter' } ] })
+
+ // Here we start extending traces using the `appendData` stream
+ const extendData = setInterval(() => {
+ appendData({ data: { y: [[Math.random() * 10]]}, tracePos: [0] });
+ }, 500);
+
+ return (
+
);
+}
+```
+
+
### Basic Props
**Warning**: for the time being, this component may _mutate_ its `layout` and `data` props in response to user input, going against React rules. This behaviour will change in the near future once https://github.com/plotly/plotly.js/issues/2389 is completed.
diff --git a/package.json b/package.json
index 945785a..5d0f853 100644
--- a/package.json
+++ b/package.json
@@ -70,7 +70,9 @@
},
"peerDependencies": {
"plotly.js": ">1.34.0",
- "react": ">0.13.0"
+ "react": ">0.13.0",
+ "flyd": ">=0.2.8",
+ "ramda": ">=0.28.0"
},
"browserify-global-shim": {
"react": "React"
diff --git a/src/usePlotly.js b/src/usePlotly.js
new file mode 100644
index 0000000..b087988
--- /dev/null
+++ b/src/usePlotly.js
@@ -0,0 +1,50 @@
+import { useLayoutEffect, useState, useMemo } from 'react';
+import { head, prop, compose, pick, objOf, mergeDeepRight } from 'ramda';
+import { stream, scan } from 'flyd';
+
+/**
+* A simple debouncing function
+*/
+const debounce = (fn, delay) => {
+ let timeout;
+
+ return function (...args) {
+ const functionCall = () => fn.apply(this, args);
+
+ timeout && clearTimeout(timeout);
+ timeout = setTimeout(functionCall, delay);
+ };
+};
+
+const getSizeForLayout = compose(objOf('layout'), pick(['width', 'height']), prop('contentRect'), head);
+
+export default function usePlotly() {
+ const updates = useMemo(stream, []);
+ const appendData = useMemo(stream, []);
+ const plotlyState = useMemo(
+ () => scan(mergeDeepRight, { data: [], config: {}, layout: {} }, updates),
+ []
+ );
+
+ const observer = new ResizeObserver(debounce(compose(updates, getSizeForLayout), 100));
+ const [internalRef, setRef] = useState(null);
+ useLayoutEffect(() => {
+ if (internalRef) {
+ observer.observe(internalRef);
+ const endS = plotlyState.map(state => {
+ Plotly.react(internalRef, state);
+ });
+
+ const endAppend = appendData.map(({ data, tracePos }) => Plotly.extendTraces(internalRef, data, tracePos));
+
+ return () => {
+ Plotly.purge(internalRef);
+ observer.unobserve(internalRef);
+ endAppend.end(true);
+ endS.end(true);
+ };
+ }
+ }, [internalRef, plotlyState, updates, appendData]);
+
+ return { ref: setRef, updates, appendData };
+}