Can React.createRef() be created dynamically from an array of state objects? - reactjs

I have a list of styled radio buttons fixed inside the sidebar that, when clicked, scroll a corresponding component from the main area into view.
Both the radio buttons list and the components inside the main area are mapped from the state array called usedComponents that contains objects of component properties and ids. Let's say the array contains 10 objects and each object looks something like this:
{
id: 1,
group: "header",
componentType: "headerLogo",
componentName: "Header 01",
//...rest of the component properties
}
In order to achieve scroll into view on radio button click I had to create references to components inside the constructor. I manually created references for each component contained inside usedComponents state array and tied them to componentType like this:
this.headerLogo = React.createRef();
this.headerLogoNavigation = React.createRef();
//...etc.
I then passed the reference to each corresponding component and in my selectComponentHandler I set scrollIntoView to call the radio button id which corresponds to the main area component reference.
The selectComponentHandler looks like this:
selectComponentHandler = e => {
const value = Number(e.target.value);
const id = e.target.id; //returns componentType. For example "headerLogo"
this.setState(prevState => {
let selected = prevState.usedComponents
.filter(item => item.id === value)
.shift();
let unSelected = prevState.usedComponents
.filter(item => item.selected === true)
.shift();
if (unSelected) {
unSelected.selected = false;
}
selected.selected = true;
return { unSelected, selected };
});
this[id].current.scrollIntoView({ block: "start", behavior: "smooth" });
};
However, I'm using react-beautiful-dnd in order to be able to add new components by dragging them to the main area, and whenever I add the new component of the type that is already contained inside the usedComponents array, it has the same reference as the old component of the same type. This makes the scrollIntoView always scroll to the component of the same type that is first in line, no matter which one I select.
Is there a way to create references dynamically for all the components inside the usedComponents array, by perhaps mapping them, and update them whenever a new component is inserted. Maybe tie them to ids?
Edit:
I tried mapping references inside the constructor like this:
this.state.usedComponents.map(component => {
const id = component.id.toString();
return (this[id] = React.createRef());
});
It works for the components that are already inside the used Components array, however I still don't know how to update the references when the new object is inserted into the usedComponents array via drag and drop. Newly inserted components basically don't have references.

Like I was mentioning in the comments, I wouldn't go the route of dynamic refs. The scope / size of these refs could grow large and there is a much better way to handle this. Just use the DOM.
When rendering you can just put the components ID on your html element that you want to scroll to
<div id={component.id}> ... </div>
And then when you want to scroll to that element just query the DOM for that element and scroll to it
const elemToScrollTo = document.getElementById(component.id)
if (!!elemToScrollTo) {
elemToScrollTo.scrollIntoView()
}

Related

How to iterate and get values from an array inside the state, that contains <ThisKindOfTags />?

I have this state inside a class component. It has two keys:
this.state = {
component: <MainScreen handleComponent={this.handleComponent} />,
position: [],
}
And I render on screen the value of component.
Every time I click a button, the event handler handleComponent does two things: updates the state 'component' value with a new component to render, and the second thing it does is to add that new component to the 'position' array, so I can have a path of components that the user went through and were rendererd on screen.
My purpose is to have a BACK button that renders the previous component on screen and the way I'm trying to do it is to read the values in the array position.
My problem is that if I do a console.log(this.state position) I get an Object.object or undefined or not a value at all. I don't get something like even that these values are inside the array position because I can see them inside the state when I go through the components when I click on them.
The event handler looks like this:
handleComponent = (value) => {
this.setState({component: value});
if (this.state.component == <MainScreen />) {
alert("it works")
}else {this.state.position.push(value)};
}
I can push the tags that are passed as 'value' to the 'position' and get the array with all the components that were rendered on screen, but I can't do something like
if (this.state.component == <MainScreen />) {something somethig}
Why that happens? It seems like I can't equal React component tags with something to get true or false, or even get a tag from the array but I can push elements in it.
Thanks!

Managing state of individual rows in react js table

I have a requirement where for each row of a table(rows are dynamically populated), I have a 'Details' button, which is supposed to pop up a modal upon clicking. How do I maintain a state for each of the rows of this table, so that React knows which row I am clicking, and hence pass the props accordingly to the modal component?
What I tried out was create an array of all false values, with length same as my data's. The plan is to update the boolean for a particular row when the button is clicked. However, I'm unable to execute the same.
Here's what I've tried so far:
let initTableState = new Array(data.length).fill(false)
const [tableState, setTableState] = useState(initTableState)
const detailsShow = (index) => {
setTableState(...tableState, tableState[index] = true)
}
I get the 'data' from DB. In the function detailsShow, I'm trying to somehow get the index of the row, so that I can update the corresponding state in 'tableState'
Also, here's what my code to put in modal component looks like, placed right after the row entries are made:
{tableState[index] && DetailModal}
Here DetailModal is the modal component. Any help in resolving this use case is greatly appreciated!
The tableState is a single array object. You are also spreading the array into the setTableState updater function, the state updater function also takes only a single state value argument or callback function to update state.
You can use a functional state update and map the previous state to the next state, using the mapped index to match and update the specific element's boolean value.
const detailsShow = (index) => {
setTableState(tableState => tableState.map((el, i) => i === index ? true : el))
}
If you don't want to map the previous state and prefer to shallow copy the array first and update the specific index:
const detailsShow = (index) => {
setTableState(tableState => {
const nextTableState = [...tableState];
nextTableState[index] = true;
return nextTableState;
})
}

Empty state from a handler function in React (with datatables)

I am using datatables.net plugin with React. I generate a button in the last column of every row using the following parameter in columnDefs of the datatables:
{
"targets": [10],
createdCell: (td, cellData, rowData, row, col) => {
ReactDOM.render(
<button onClick={props.handleReportClick}></button>
, td);
}
})
Within my react page, I have the following:
const handleReportClick = (e) =>{
e.stopPropagation();
console.log(dataTable);
};
The console.log above gives me empty string (there is a hook [dataTable, setDataTable] on the page which has values immediately after initiation of the datatable) and same happens for all other hook values on that page. All other event handlers on the page console.log(dataTable) as an object and can access all state but not this one.
Any help appreciated.
I have found that passing reference to the handler function to the button (as in my example above) was giving me all sorts of problems with the handler function referencing wrong render instances. Instead, I provided each button with a unique data-id (in my case row was enough as I only have one button in a row) and then created a useEventListener hook to add event listener to all clicks on the page. I also added the below function to get the data-id of the button that was clicked as the event listener was unreliable and yielded child elements as targets. Below works well and captures data-id each time. This way, the state that I access for dataTable is correct and I can capture info from datatable's appropriate row and process it.
export default function getSelectedRow(e) {
let parent = e.target;
let selectedRow;
while (parent) {
if (parent.getAttribute('data-id')) {
selectedRow = parent.getAttribute('data-id');
}
parent = parent.parentElement;
}
return selectedRow;
}

How to click unrendered virtualized element in TestCafe

I'm using react-virtualized for a lengthy (1000+) list of items to select from. And I'm trying to set up an end to end test that requires clicking on one of the elements that are not currently rendered.
Ordinarily, I'd simply use something like:
await t.click(
ReactSelector('ListPage')
.findReact('ListItem')
.nth(873) // or .withText(...) or .withProps(...)
)
But because only a small subset of the ListItems are rendered, TestCafe fails to find the desired element.
I've been trying to figure out how to use TestCafe's ClientFunction to scroll the list container so the desired ListItem is rendered.
However, I'm running into a few issues:
Is there a way to share a Selector into the ClientFunction and modify the DOM element's scrollTop? Or do I have to re-query the element via the DOM directly?
Due to the ListItems being different heights, the scroll position is not a simple calculation of index x item height. How can I keep updating/scrolling within this function until the desired Selector is visible?
Is there a way to share a Selector into the ClientFunction and modify the DOM element's scrollTop?
There is a way to put Selector into the Client Function. Please refer to this example in the TestCafe documentation.
How can I keep updating/scrolling within this function until the desired Selector is visible?
You can use the TestCafe exist property to check if the element is rendered or not. The following example demonstrates the approach:
import { Selector } from 'testcafe';
fixture`Getting Started`.page('https://bvaughn.github.io/react-virtualized/#/components/List')
test('Test 1', async t => {
const dynamicRowHeightsInput = Selector('._1oXCrgdVudv-QMFo7eQCLb');
const listItem = Selector('._113CIjCFcgg_BK6pEtLzCZ');
const targetItem = listItem.withExactText('Tisha Wurster');
await t.click(dynamicRowHeightsInput);
while (!await targetItem.exists) {
const currentLastRenderdItemIndex = await listItem.count -1;
const currentLastRenderdItemText = await listItem.nth(currentLastRenderdItemIndex).textContent;
const currentLastRenderdItem = await listItem.withExactText(currentLastRenderdItemText);
await t.hover(currentLastRenderdItem);
}
await t
.hover(targetItem)
.click(targetItem);
await t.debug();
});
To scroll the list container I used the hover action with a last rendered listItem as a target element.

How do I manage UI state among subcomponents?

I want to display a selectable list of objects with react. I've got the list component, which owns several groups, which own several elements. Obviously, the list data itself should go into a store, but what about the UI state (e.g., which element is selected)? It's ostensively a property of the root list element, but to pass it down to the elements, each element along the chain (well, just 1 in this case- the group) would need to pass it along even if they don't care about it. And if I want to add another property I need to pass it down the chain rather verbosely.
Also, encapsulation. How do I make a component that might be instanced multiple times with different data?
In other languages I would just pass the list pointer down the chain, so that child elements could whatever they want from it, so everything stays encapsulated. Would the flux way be to pass the store down to child elements? Would you have a separate store for UI state vs the persistent data?
Let's design this from the bottom up. So, at the bottom we have a ListItem.
What does the list item need in order to render? Let's say it's a title, a body, and if it's selected or not.
Also what events make sense of the list item? Probably just onClick.
Does it have any state? No, unless we need to handle things like it being hovered, or other state specific to the ListItem, but not relevant to its parent.
var ListItem = React.createClass({
render(){
var classNames = ["list-item"];
if (this.props.selected) classNames.push("selected");
return (
<li className={classNames.join} onClick={this.props.onClick}>
<div className="title">{this.props.title}</div>
<div className="body">{this.props.body}</div>
</li>
);
}
});
The ListGroup is a bit less obvious.
For props we'll ask for a list of items, the selected index (if any), and an onSelectionChange callback, which is called with (nextIndex, lastIndex).
This component also has no state. For extra points, we'll allow specifying a custom renderer, making this more reusable. You can pass renderer={component} where component is something that implements the ListItem interface described above.
var ListGroup = React.createClass({
getDefaultProps: function(){
return {renderer: ListItem, onSelectionChange: function(){}}
},
render(){
return (
<div>{this.props.items.map(this.renderItem)}</div>
);
},
renderItem(data, index){
var selectedIndex = this.props.selectedIndex;
return (
<this.props.renderer
selected={selectedIndex === index}
key={i}
onClick={() => this.props.onSelectionChange(index, selectedIndex)}
{...data} />
}
});
And then we could render ListGroup like this:
<ListGroup
items={[{title: "foo", body: "bar"}, {title: "baz", body: "quux"]}
selectedIndex={this.state.i}
onSelectionChange={(index) => this.setState({i: index})} />
The data is passed down the tree, but only the essence of the data required to render each component. ListItem doesn't need the full list of items, nor the selected index. It doesn't care if multiple selections are allowed, or just one. All it knows is that it's selected when this.props.selected is truthy, and otherwise it's not.

Resources