Updating array state in react - reactjs

if I have a bunch of boxes and each box has 'data-index'
<div
ref={ref}
className={isClicked ? "box clicked" : " box"}
data-index={img.num}
onClick={BoxClicked}
>
and if I clicked to one of them I want to get the data-index for that box and store it in array of state
so If I clicked to two boxes and box one has data-index=2, and box two has data-index=4
the expected output will be array of these value [2,4]
I try this but for some reasons it didn't work
what happen is when I clicked to another box the previous value is gone and the array is updated to newest data-index's box
const [index, setIndex] = useState([]);
const BoxClicked = () => {
setIsClicked(true);
setIndex((pre) => [...pre, ref.current.dataset.index]);
// setIndex([...index, ref.current.dataset.index]);
};

Related

d3.js Tooltip do not receive data properly

I have d3 line chart with two lines full with events and made a html tooltip to show some data from these events on mousemove. Its working fine until the moment when you switch to show only one line, than the tooltip doesnt receive any data. Ive log the data and it is coming to the tooltip, but tooltip is not receiving it for some reason. Check the code bellow:
const tooltip = d3.select('.Tooltip')
.style('visibility', 'hidden')
.style('pointer-events', 'none')
function mousemove (event) {
// recover coordinate we need
const mouse = d3.pointer(event, this)
const [xm, ym] = (mouse)
const mouseWindows = d3.pointer(event, d3.select(this))
const [xmw, ymw] = (mouseWindows)
const i = d3.least(I, i => Math.hypot(xScale(X[i]) - xm, yScale(Y[i]) - ym)) // closest point
path.style('stroke-width', ([z]) => Z[i] === z ? strokeWidthHovered : strokeWidth).filter(([z]) => Z[i] === z).raise()
dot.attr('transform', `translate(${xScale(X[i])},${yScale(Y[i])})`)
// console.log(O[i]) <-- this is the data for the current selected event and its fine after
changing the line shown
tooltip
.data([O[i]])
.text(d => console.log(d)) <-- here i log what is coming from data and doesn`t return anything
.style('left', `${tooltipX(xScale(X[i]), offSetX)}px `)
.style('top', `${tipY}px`)
.style('visibility', 'visible')
.attr('class', css.tooltipEvent)
.html(d => chartConfig.options.tooltip.pointFormat(d))
When you call tooltip.data([0[i]]), d3 returns a selection of elements already associated with your data, which does not make sense for your tooltip. If you already computed the value you want to show (I assume it is O[i]), then simply set the text or the html of the tooltip:
tooltip.html(chartConfig.options.tooltip.pointFormat(O[i]);

React, NextJS: Optimizing performance for many child components that all need to re-render/update

I've got a parent component (Grid), which generates a number of vertical rows. Each row horizontally generates a number of cell components. Each Cell component vertically generates four subcells. I've simplified it like so:
// SubCell.tsx
export default function SubCell(row, column) {
return <div>{row},{column}</div>
}
// Cell.tsx
function Cell(row, column) {
const generateCell = () => {
const subcells = [];
for (let i=0; i<4; i++){
subcells.push(<SubCell row={row+i} column={column}/>)
}
}
return <div>{generateCell()}</div>;
}
// Grid.tsx
export default function Grid(){
const horizontalLabels = [...];
const verticalLabels = [...];
const generateHeaders = () => {
return verticalLabels.map((label, index) => {
<div>{label}</div>
});
}
const generateRows = () => {
return (verticalLabels.map((vLabel, columnIndex) => {
return (horizontalLabels.map((hLabel, rowIndex) => {
return (<Cell row={rowIndex*4} column={columnIndex}/>);
});
});
}
return (
<div>
{generateHeaders()}
{generateRows()}
</div>
)
}
The components are in separate files, but I've included them together for simplicity.
My goal is to select an area of subcells; meaning, I can click my mouse down on one subcell, move it over other subcells, and the area of cells that are in the range of the mousedown cell and the mouseover cell will be highlighted. Currently, I am accomplishing this through a shared state kept in context, and passed down through Grid to cells and subcells.
function SubCell(row, column){
const {currentMouseDownCell, currentMouseOverCell, setCurrentMousedownCell, setCurrentMouseOverCell} = useContext(GridContext);
... the rest
return <div
onMouseDown{setCurrentMousedownCell(row,column)}
onMouseOver={setCurrentMouseOverCell(row,column)>
{row},{column}
</div>
}
export default function Grid(){
...// everything else
return (
<GridContext>
<div>
{generateHeaders()}
{generateRows()}
</div>
</GridContext>
)
}
// GridContext.ts
export interface GridContextType {
currentMouseDownCell: number[];
currentMouseOverCell: number[];
setCurrentMouseDownCell: Dispatch<SetStateAction<number[]>>;
setCurrentMouseOverCell: Dispatch<SetStateAction<number[]>>;
}
export const GridContext = createContext<GridContextType>(
{} as GridContextType
);
export const GridContextProvider = (children) => {
const [currentMouseDownCell, setCurrentMouseDownCell] = useState([-1, -1]);
const [currentMouseOverCell, setCurrentMouseOverCell] = useState([-1, -1]);
const GridContextValue = {
currentMouseDownCell,
setCurrentMouseDownCell,
currentMouseOverCell,
setCurrentMouseOverCell,
}
return (
<GridContext.Provider value={GridContextValue}>
{children}
</GridContext.Provider>
)
}
And then for each SubCell, I'm checking for any changes to the current mousedown and mouseover subcells. If there is a change, I check if the current subcell is in between the two. If so, then I change the background to the highlighted color. If not, I change it / keep it white.
function SubCell(row, column) {
const [currentColor, setCurrentColor] = useState("white");
const {currentMouseDownCell, currentMouseOverCell, setCurrentMousedownCell, setCurrentMouseOverCell} = useContext(GridContext);
useEffect(()=>{
if row and column are in between the mousedown and mouseover cells
setCurrentColor("blue");
else setCurrentColor("white");
}, [currentMouseDownCell, currentMouseOverCell]);
return <div style={{backgroundColor: currentColor}}>{row},{column}</div>
}
This works, but with large numbers of subcells that all re-render every time a mouseover changes, it can get somewhat choppy / slow, and I'm looking for a smooth experience. Any suggestions on speed increases?
What I've tried:
Using , but it's the same performance and I need a more customizable layout.
Memo, but I don't see how I can avoid the re-render since every subcell needs to check if it should be highlighted every time.
Virtualization doesn't help, because the cells will all be visible.
Prop drilling instead of context, but that didn't make it much faster.
Most online resources try to prevent components from doing the computation to re-render, but in my case it seems unavoidable.
Edit 04/17/22:
After reading React memo keeps rendering when props have not changed and this one I'm trying to make a grid of clickable boxes where boxes know which other boxes they're adjacent to. JS/React I realized that it's better to keep the data for all cells stored at the parent level, rather than modulated individually.
I've created a 2D array, and then I've passed it down as props to the cell, and then the individual values to each subcell. This way, the parent can have the useEffect for mousedown and mouseover, do the appropriate calculations on every subcell, and then update just the relevant segments of the table. Through memoization, I can ensure that only the affected subcells are re-rendered (by checking that their old cell value is the same as the new one).

Canvas onclick event react

Hello and thank you for reading this question.
I'm struggling to deal with the onclick event on canvas with React.
I am currently building a component that has to draw bounding boxes on an image and make those boxes fire a onclick event with its coordinates and/or metadata.
I am using the following react-bounding-box component (https://www.npmjs.com/package/react-bounding-box) with a bit of customization.
The idea is that my component receive the following data :
an image
a JSON with a list of items that contains coordinates of bounding boxes and other data related to those boxes.
Once the JSON is loaded, my code iterates on the list of items and draws the bounding boxes on the image using canvas.
My component definition looks like that (I omitted useless lines of code) :
[...]
import BoundingBox from 'react-bounding-box'
[...]
export const ComicImageDrawer = (props) => {
const [boundingBoxesItems, setBoundingBoxesItems] = useState(Array<any>());
const [selectedBoxItem, setSelectedBoxItem] = useState({})
const [selectedBoxIndex, setSelectedBoxIndex] = useState<Number>(-1);
const [currentImageBoxes, setCurrentImageBoxes] = useState(Array<any>())
useEffect(() => {
[...] // loading data
}, [])
// That function is fired when a box is hovered
// param value is the index of the box
// I would like to do the same but with the `onclick` event
function onOver(param) {
[...] // don't care
}
const params = {
[...] // don't care
}
};
return (
<BoundingBox
image={currentImage}
boxes={currentImageBoxes}
options={params.options}
onSelected={onOver}
drawBox={drawBoxCustom}
drawLabel={() => {}}
/>
)
}
The redefined the component drawBox() function to add some customization. So that function definition looks like this :
function drawBoxCustom(canvas, box, color, lineWidth) {
if(!box || typeof box === 'undefined')
return null;
const ctx = canvas.getContext('2d');
const coord = box.coord ? box.coord : box;
let [x, y, width, height] = [0, 0, 0, 0]
[...] // removed useless lines of codes
ctx.lineWidth = lineWidth;
ctx.beginPath();
[...] // drawing element definition
ctx.stroke();
};
I haved tried the following stuff to make the canvas fire an onclick event but it never fires (i also tried other event like mouseover) :
// Listen for mouse moves
canvas.addEventListener('onmouseover', function (event) {
console.log('click event', event);
});
What I would like to obtain is to fire a function in my React component that looks like that. The idea is to determine which box has been clicked :
const handleCanvasClick = (event, box) => {
console.log('event', event);
console.log('box', box);
}
Any help or suggestion would be appreciated.
Thanks.

Is there a better approach to implementing Google Sheet's like cell functionality?

I'm building out a system in React that has tabular data with cells. Those cells are editable via contentEditable divs. It's functionally similar to google sheets. I'm working on the functionality where single click on the cell allows the user to override the current value of the cell and double clicking allows them to edit the value.
The functionality involved is basically this:
When single click on cell override the current value. (No cursor visible?)
When double click on cell allow the user to edit the current value. (Cursor visible, can move left and right of chars with arrowKeys)
When double clicked into the cell reformat value (removes trailing zero's for cents: 8.50 becomes 8.5)
When double clicked start the caret position at the end of the input.
When user clicks out of the cells reformat the current value to its appropriate format (example is a price cell)
My cell component looks like this:
(Note* useDoubleClick() is a custom hook I wrote that works perfectly fine and will call single/double click action accordingly)
export default function Cell({ name, value, updateItem }) {
const [value, setValue] = useState(props.value), // This stays uncontrolled to prevent the caret jumps with content editable.
[isInputMode, setIsInputMode] = useState(false),
cellRef = useRef(null);
// Handle single click. Basically does nothing right now.
const singleClickAction = () => {
if(isInputMode)
return;
}
// Handle double click.
const doubleClickAction = () => {
// If already input mode, do nothing.
if(isInputMode) {
return;
}
setIsInputMode(true);
setCaretPosition(); // Crashing page sometimes [see error below]
reformatValue();
}
// It's now input mode, set the caret position to the length of the cell's innerText.
const setCaretPosition = () => {
var range = document.createRange(),
select = window.getSelection();
range.setStart(cellRef.current.childNodes[0], cellRef.current.innerText.length);
range.collapse(true);
selectObject.removeAllRanges();
selectObject.addRange(range);
}
// Reformat innerText value to remove trailing zero's from cents.
const reformatValue = () => {
var updatedValue = parseFloat(value);
setValue(updatedValue);
}
const onClick = useDoubleClick(singleClickAction, doubleClickAction);
/*
* Handle input change. Pass innerText value to global update function.
* Because we are using contentEditable and render "" if !isInputMode
* we have override functionality.
*/
const onInput = (e) => {
props.updateItem(props.name, e.target.innerText);
}
// When cell is blurred, reset isInputMode
const onBlur = () => {
setIsInputMode(false);
cellRef.current.innerText = ""; // Without this single click will not override again after blur.
}
return (
<div
data-placeholder={value} // to view current value while isInputMode is false
class="cell-input"
contentEditable="true"
onClick={onClick}
onInput={onInput}
onBlur={onBlur}
ref={cellRef}
>
{isInputMode ? value : ""}
</div>
)
}
And here is some css so that the user can see the current value while isInputMode is false:
.cell-input {
:empty:before {
content: attr(data-placeholder);
}
:empty:focus:before {
content: attr(data-placeholder);
}
}
Now here are the issues I'm running into.
When I call the setCaretPosition function, there are no childNodes because I'm rendering the empty value ("") and crashes the page sometimes with the error- TypeError: Argument 1 ('node') to Range.setStart must be an instance of Node.
I have a $ inside cells that contain a price and I was setting that in the css with ::before and content: '$', but now I can't because of the data-placeholder snippet.
When double clicking into cell the cursor is not visible at all. If you click the arrows to move between characters it then becomes visible.
This solution has me pretty close to my desired output so I feel pretty good about it, but I think there might be a better way to go about it or a few tweaks within my solution that will be a general improvement. Would love to hear some ideas.

react-native-multiple-select storing items selected on submit

I am using react-native-multiple-select and trying to create a dropdown menu that allows users to select multiple options and then logs the options they select into an array.
At the moment, my code is:
onSelectedItemsChange = selectedItems => {
this.setState({ selectedItems });
console.log('submit button was pressed')
};
render() {
const { selectedItems } = this.state;
return (
<View style={{ flex: 1 }}>
<MultiSelect
hideTags
items={items}
uniqueKey="id"
ref={(component) => { this.multiSelect = component }}
onSelectedItemsChange={this.onSelectedItemsChange}
selectedItems={selectedItems}
selectText="Pick Items"
searchInputPlaceholderText="Search Items..."
onChangeInput={ (text)=> console.log(text)}
altFontFamily="ProximaNova-Light"
tagRemoveIconColor="#CCC"
tagBorderColor="#CCC"
tagTextColor="#CCC"
selectedItemTextColor="#CCC"
selectedItemIconColor="#CCC"
itemTextColor="#000"
displayKey="name"
searchInputStyle={{ color: '#CCC' }}
submitButtonColor="#CCC"
submitButtonText="Submit"
/>
<View>
The problem is with the submit button. I only want to record the items selected once the user has pressed submit.
At the moment it logs that the button was pressed every time a new item is selected and that does not help with storing the items selected into another array.
Any help would be great.
You can do this to get an array with the item objects that are selected:
for(var i = 0; i < selectedItems.length; i++){
this.state.selectedItemsArray.push(this.state.gasOptions[selectedItems[i]])
}
console.log(selectedItems);
This should output the array of items that are selected with each item containing the unique key and the display name.
this.state.selectedItemsArray.push(listOfObject[0].id);
I noticed that the selectedItemsArray stores only the key so its an array of keys and not list of objects. Thus, if your key is id you want to push that to the array and not all the object.
I faced the same issue before. Now I fixed it.
Follow below steps:
Go to node_modules/react-native-multi-select/index.d.ts add the code
onSubmitclick: ((items: any[]) => void), inside export interface MultiSelectProps {}
Go to lib/react-native-multi-select.js add the code
onSubmitclick: PropTypes.func, inside the static propTypes ={}
Go to the function _submitSelection() and add the code inside it
const {selectedItems, onSubmitclick } = this.props; onSubmitclick(selectedItems);
Now you return your Multiselect tag add
onSubmitclick={(value1) => getSubmit(value1)}
capture your selected value with this function
const getSubmit = (value1) => { console.log('new submit value***', value1) }
I hope, It will helpful for someone.

Resources