React Highcharts - how to use drillUp from parent component? - reactjs

I am trying to drillUp from the parent component
This is the father component:
<button onClick={() => {
drillUp();
}}></button>
<TreeMap dataLabel={dataLabel} height={blockHeight} />
And in the child component I am rendering the Highchart
<ReactHighcharts
highcharts={Highcharts}
options={treeConfig}
></ReactHighcharts>
This is my treeConfig
const treeConfig = {
chart: {
height: height,
events: {
render() {
const elements = document.getElementsByClassName(
"highcharts-point highcharts-color-0 highcharts-internal-node-interactive",
);
const drillButton = document.getElementsByClassName(
"highcharts-drillup-button",
)[0];
if (drillButton && drillButton.id !== "drill") {
drillButton.setAttribute("id", "drill");
}
if (elements.length) {
const sortElements = [...elements].sort((a, b) => {
return a.width.baseVal.value * a.height.baseVal.value >
b.width.baseVal.value * b.height.baseVal.value
? -1
: 1;
});
sortElements.forEach((el, i) => {
el.style.setProperty("fill", colorPallete[i], "important");
el.setAttribute("fill", colorPallete[i]);
});
}
}
},
},
I found this post -> Manually Triggering drillup event, highcharts but it's in jQuery and I tried that and it didn't work
How can I cause drillUp event from the parent component in React?
Thanks in advance!

Ok, this was more complicated than I thought because the documentation is dreadful but I was able to get it running on this stackblitz
To be able to call events manually or programmatically on the chart we need to get the reference to the chart object. To get the ref on the child component we can do something like this:
const Child = ({ setChart }) => {
...
const chartComponent = useRef(null);
useEffect(() => {
setChart(chartComponent.current.chart);
}, []);
highchartsDrillDown(Highcharts);
return (
<HighchartsReact
highcharts={Highcharts}
options={options}
ref={chartComponent}
/>
);
}
setChart is a prop from the parent that sets the chart object on the parent
On the parent you can manually call drilldown and drillup like this:
const Parent = () => {
const [chart, setChart] = useState(null);
const handleDrillUp = () => {
chart.drillUp();
};
const handleDrillDown = () => {
chart.series[0].data[0].doDrilldown(); // you can chose the series and data
};
return (
<div>
<button onClick={handleDrillUp}> drill up</button>
<button onClick={handleDrillDown}> drill down</button>
<Child setChart={setChart} />;
</div>
);
};
EDIT:
The previous solution is for the highcharts-react-official lib.
Just noticed you are using a different lib for highcharts than the one I used, so for react-highcharts the only difference would be:
const afterRender = (chart) => {setChart(chart)};
<ReactHighcharts config = {config} callback = {afterRender}>.
</ReactHighcharts>
the only difference is that one uses the useRef hook and the other has a callback prop

Related

Detect If Function Runs on Another Component in React

I need to detect if handleSelectProduct is being called in another component.
My problem is that if I want the child component(ProductDetailsComponent) to rerender, it still outputs the console.log('HELO'). I only want to output the console.log('HELO') IF handleSelectProduct is being click only.
const ProductComponent = () => {
const [triggered, setTriggered] = React.useState(0);
const handleSelectProduct = (event) => {
setTriggered(c => c + 1);
};
return (
<div>
Parent
<button type="button" onClick={handleSelectProduct}>
Trigger?
</button>
<ProductDetailsComponent triggered={triggered} />
</div>
);
};
const ProductDetailsComponent = ({ triggered }) => {
React.useEffect(() => {
if (triggered) {
console.log('HELO');
}
}, [triggered]);
return <div>Child</div>;
};
ReactDOM.render(
<ProductComponent />,
document.getElementById("root")
);
The simplest solution sounds to me by using an useRef to keep the old value, thus consider the console.log only when the triggered value changes.
const ProductDetailsComponent = ({ triggered }) => {
const oldTriggerRef = React.useRef(0);
React.useEffect(() => {
if (triggered !== oldTriggerRef.current) {
oldTriggerRef.current = triggered;
console.log('HELO');
}
}, [triggered]);
return <div>Child</div>;
};

Pass/call function from child to parent in React

I'm slowly learning React and ran into a little problem. I have created my own component (functional approach) which is represented by the grid.
Grid component has functions in it that create a new blank line. The button that should trigger the add action I had to pull higher, to the parent.
I have read a bit and would like to skip the use of refs to trigger an action from the child relative to clicking on the parent.
I wrote this code, it works as I want, but I would like to ask you to check and tell if this approach is correct or if I should do it differently.
Below I present an important fragment of the components.
const View = () => {
const [add, setAdd] = useState(() => { });
const callbackOnAdd = (handle) => {
setAdd({ onAdd: handle });
}
const handleButtonClick = useCallback(() => {
add.onAdd();
});
return (
<Grid callbackOnAdd={callbackOnAdd} />
<Button onClick={handleButtonClick} />
);
}
const Grid = ({ callbackOnAdd }) => {
...
useEffect(() => {
callbackOnAdd(() => {
addNewRow();
});
}, []);
...
};

How to change styling for only one of same children depend on prop

I am trying to implement star voting with font awesome icons,
on Product.js i have 2 component , problem is when i change styling for a component that has prop, naturally it changes other component because of querySelectorAll, so how can i change the class for only component which has props i pass.
Product.js
const [rating, setRating] = useState(0);
const ratingHandler = (e) => {
setRating(e);
};
<Rating handleRating={ratingHandler} />
<Rating />
for first child i want to add some styling like when i mouse over star ,it lights up etc. So i want to make it based on handleRating props.
Rating.js
const Rating = (handleRating = false) => {
useEffect(() => {
if (handleRating) {
hover();
}
}, []);
function hover() {
const spans = document.querySelectorAll(".ratinger span:not(.texting)");
console.log(spans);
spans.forEach((spanon) => {
const onStar = parseInt(spanon.dataset.value, 10);
spanon.onmouseover = () => {
spans.forEach((span) =>
span.dataset.value <= onStar
? span.classList.add("hover")
: span.classList.remove("hover")
);
};
spanon.onmouseout = () => {
spans.forEach((span) => {
span.classList.remove("hover");
});
};
spanon.onclick = () => {
spans.forEach((span) =>
span.dataset.value <= onStar
? span.classList.add("onclick")
: span.classList.remove("onclick")
);
handleRating(parseInt(spanon.dataset.value, 10));
};
});
}
i didnt add unnecessary parts on below i have icons within a div.
Try:
const node = ReactDOM.findDOMNode(this);
const spans = node.querySelectorAll('...');

How to optimize React components with React.memo and useCallback when callbacks are changing state in the parent

I've come accross a performance optimization issue that I feel could be fixed somehow but I'm not sure how.
Suppose I have a collection of objects that I want to be editable. The parent component contains all objects and renders a list with an editor component that shows the value and also allows to modify the objects.
A simplified example would be this :
import React, { useState } from 'react'
const Input = props => {
const { value, onChange } = props
handleChange = e => {
onChange && onChange(e.target.value)
}
return (
<input value={value} onChange={handleChange} />
)
}
const ObjectEditor = props => {
const { object, onChange } = props
return (
<li>
<Input value={object.name} onChange={onChange('name')} />
</li>
)
}
const Objects = props => {
const { initialObjects } = props
const [objects, setObjects] = useState(initialObjects)
const handleObjectChange = id => key => value => {
const newObjects = objects.map(obj => {
if (obj.id === id) {
return {
...obj,
[key]: value
}
}
return obj
})
setObjects(newObjects)
}
return (
<ul>
{
objects.map(obj => (
<ObjectEditor key={obj.id} object={obj} onChange={handleObjectChange(obj.id)} />
))
}
</ul>
)
}
export default Objects
So I could use React.memo so that when I edit the name of one object the others don't rerender. However, because of the onChange handler being recreated everytime in the parent component of ObjectEditor, all objects always render anyways.
I can't solve it by using useCallback on my handler since I would have to pass it my objects as a dependency, which is itself recreated everytime an object's name changes.
It seems to me like it is not necessary for all the objects that haven't changed to rerender anyway because the handler changed. And there should be a way to improve this.
Any ideas ?
I've seen in the React Sortly repo that they use debounce in combination with each object editor changing it's own state.
This allows only the edited component to change and rerender while someone is typing and updates the parent only once if no other change event comes up in a given delay.
handleChangeName = (e) => {
this.setState({ name: e.target.value }, () => this.change());
}
change = debounce(() => {
const { index, onChange } = this.props;
const { name } = this.state;
onChange(index, { name });
}, 300);
This is the best solution I can see right now but since they use the setState callback function I haven't been able to figure out a way to make this work with hooks.
You have to use the functional form of setState:
setState((prevState) => {
// ACCESS prevState
return someNewState;
});
You'll be able to access the current state value (prevState) while updating it.
Then way you can use the useCallback hook without the need of adding your state object to the dependency array. The setState function doesn't need to be in the dependency array, because it won't change accross renders.
Thus, you'll be able to use React.memo on the children, and only the ones that receive different props (shallow compare) will re-render.
EXAMPLE IN SNIPPET BELOW
const InputField = React.memo((props) => {
console.log('Rendering InputField '+ props.index + '...');
return(
<div>
<input
type='text'
value={props.value}
onChange={()=>
props.handleChange(event.target.value,props.index)
}
/>
</div>
);
});
function App() {
console.log('Rendering App...');
const [inputValues,setInputValues] = React.useState(
['0','1','2']
);
const handleChange = React.useCallback((newValue,index)=>{
setInputValues((prevState)=>{
const aux = Array.from(prevState);
aux[index] = newValue;
return aux;
});
},[]);
const inputItems = inputValues.map((item,index) =>
<InputField
value={item}
index={index}
handleChange={handleChange}
/>
);
return(
<div>
{inputItems}
</div>
);
}
ReactDOM.render(<App/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"/>
Okay, so it seems that debounce works if it's wrapped in useCallback
Not sure why it doesn't seem to be necessary to pass newObject as a dependency in the updateParent function though.
So to make this work I had to make the following changes :
First, useCallback in the parent and change it to take the whole object instead of being responsible for updating the keys.
Then update the ObjectEditor to have its own state and handle the change to the keys.
And wrap the onChange handler that will update the parent in the debounce
import React, { useState, useEffect } from 'react'
import debounce from 'lodash.debounce'
const Input = props => {
const { value, onChange } = props
handleChange = e => {
onChange && onChange(e.target.value)
}
return (
<input value={value} onChange={handleChange} />
)
}
const ObjectEditor = React.memo(props => {
const { initialObject, onChange } = props
const [object, setObject] = useState(initialObject)
const updateParent = useCallback(debounce((newObject) => {
onChange(newObject)
}, 500), [onChange])
// synchronize the object if it's changed in the parent
useEffect(() => {
setObject(initialObject)
}, [initialObject])
const handleChange = key => value => {
const newObject = {
...object,
[key]: value
}
setObject(newObject)
updateParent(newObject)
}
return (
<li>
<Input value={object.name} onChange={handleChange('name')} />
</li>
)
})
const Objects = props => {
const { initialObjects } = props
const [objects, setObjects] = useState(initialObjects)
const handleObjectChange = useCallback(newObj => {
const newObjects = objects.map(obj => {
if (newObj.id === id) {
return newObj
}
return obj
})
setObjects(newObjects)
}, [objects])
return (
<ul>
{
objects.map(obj => (
<ObjectEditor key={obj.id} initialObject={obj} onChange={handleObjectChange} />
))
}
</ul>
)
}
export default Objects

changes to state issued from custom hook not causing re-render even though added to useEffect

I have a custom hook that keeps a list of toggle states and while I'm seeing the internal state aligning with my expectations, I'm wondering why a component that listens to changes on the state kept by this hook isn't re-rendering on change. The code is as follows
const useToggle = () => {
const reducer = (state, action) => ({...state, ...action});
const [toggled, dispatch] = useReducer(reducer, {});
const setToggle = i => {
let newVal;
if (toggled[i] == null) {
newVal = true;
} else {
newVal = !toggled[i];
}
dispatch({...toggled, [i]: newVal});
console.log('updated toggled state ...', toggled);
};
return {toggled, setToggle};
};
const Boxes = () => {
const {setToggle} = useToggle()
return Array.from({length: 8}, el => null).map((el,i) =>
<input type="checkbox" onClick={() => setToggle(i)}/>)
}
function App() {
const {toggled} = useToggle()
const memoized = useMemo(() => toggled, [toggled])
useEffect(() => {
console.log('toggled state is >>>', toggled) // am not seeing this on console after changes to toggled
}, [toggled])
return (
<div className="App">
<Boxes />
</div>
);
}
It's because you are using useToggle twice.
once in the App
another one in the Boxes.
When you dispatch the action in Boxes, it's updating the toggled instance for Boxes (which is not retrieved in it).
Think of your custom hook like how you use useState. When you use useState, each component gets its own state. Same goes for the custom hook.
So there are a few ways you can address the issue.
Pass the setToggle from App to Boxes via prop-drilling
Use Context API (or Redux or other statement management library to pass
setToggle instance in the App component down)
Here is an example of prop-drilling.
You can follow along
const Boxes = ({ setToggle }) => {
// const { setToggle } = useToggle();
return Array.from({ length: 8 }, el => null).map((el, i) => (
<input key={i} type="checkbox" onClick={() => setToggle(i)} />
));
};
function App() {
const { toggled, setToggle } = useToggle();
useEffect(() => {
console.log("toggled state is >>>", toggled); // am not seeing this on console after changes to toggled
}, [toggled]);
return (
<div className="App">
<Boxes setToggle={setToggle} />
</div>
);
}
Note: that I added key props in Boxes using the index i(and it is a bad practice by the way)
You can see that it's now working as you'd expect.

Resources