React Window How to Pass in Components? - reactjs

I am trying to implement react-window but Ia m not sure how to pass in a component that takes in it's own properties
If I have something like this
{
items.map(
(item, index) => {
<MyComponent
key={key}
item={item}
/>;
}
);
}
how do I make a variable list?
The example does not show how to do this
import { VariableSizeList as List } from 'react-window';
// These row heights are arbitrary.
// Yours should be based on the content of the row.
const rowHeights = new Array(1000)
.fill(true)
.map(() => 25 + Math.round(Math.random() * 50));
const getItemSize = index => rowHeights[index];
const Row = ({ index, style }) => (
<div style={style}>Row {index}</div>
);
const Example = () => (
<List
height={150}
itemCount={1000}
itemSize={getItemSize}
width={300}
>
{Row}
</List>
);

The example is indeed confusing. This example shows how you can use react-window with an array of data instead of the generative example that the homepage shows:
class ComponentThatRendersAListOfItems extends PureComponent {
render() {
// Pass items array to the item renderer component as itemData:
return (
<FixedSizeList
itemData={this.props.itemsArray}
{...otherListProps}
>
{ItemRenderer}
</FixedSizeList>
);
}
}
// The item renderer is declared outside of the list-rendering component.
// So it has no way to directly access the items array.
class ItemRenderer extends PureComponent {
render() {
// Access the items array using the "data" prop:
const item = this.props.data[this.props.index];
return (
<div style={this.props.style}>
{item.name}
</div>
);
}
}
While itemsArray is not provided in the code sample, ostensibly you would include the props you need to pass to ItemRenderer in it, such as name as shown here. This would leave your usage looking something like this:
<ComponentThatRendersAListOfItems
itemsArray={[{ name: "Test 1" }, { name: "Test 2" }]}
/>

Related

Like Button with Local Storage in ReactJS

I developed a Simple React Application that read an external API and now I'm trying to develop a Like Button from each item. I read a lot about localStorage and persistence, but I don't know where I'm doing wrong. Could someone help me?
1-First, the component where I put item as props. This item bring me the name of each character
<LikeButtonTest items={item.name} />
2-Then, inside component:
import React, { useState, useEffect } from 'react';
import './style.css';
const LikeButtonTest = ({items}) => {
const [isLike, setIsLike] = useState(
JSON.parse(localStorage.getItem('data', items))
);
useEffect(() => {
localStorage.setItem('data', JSON.stringify(items));
}, [isLike]);
const toggleLike = () => {
setIsLike(!isLike);
}
return(
<div>
<button
onClick={toggleLike}
className={"bt-like like-button " + (isLike ? "liked" : "")
}>
</button>
</div>
);
};
export default LikeButtonTest;
My thoughts are:
First, I receive 'items' as props
Then, I create a localStorage called 'data' and set in a variable 'isLike'
So, I make a button where I add a class that checks if is liked or not and I created a toggle that changes the state
The problem is: I need to store the names in an array after click. For now, my app is generating this:
App item view
localStorage with name of character
You're approach is almost there. The ideal case here is to define your like function in the parent component of the like button and pass the function to the button. See the example below.
const ITEMS = ['item1', 'item2']
const WrapperComponent = () => {
const likes = JSON.parse(localStorage.getItem('likes'))
const handleLike = item => {
// you have the item name here, do whatever you want with it.
const existingLikes = likes
localStorage.setItem('likes', JSON.stringify(existingLikes.push(item)))
}
return (<>
{ITEMS.map(item => <ItemComponent item={item} onLike={handleLike} liked={likes.includes(item)} />)}
</>)
}
const ItemComponent = ({ item, onLike, liked }) => {
return (
<button
onClick={() => onLike(item)}
className={liked ? 'liked' : 'not-liked'}
}>
{item}
</button>
)
}
Hope that helps!
note: not tested, but pretty standard stuff

Observe (get sized) control (listen to events) over a nested component in the react and typescript application via the forwardRef function

I have a functional component called MyDivBlock
const MyDivBlock: FC<BoxProps> = ({ }) => {
{getting data...}
return (
<>
<div className='divBlock'>
{data.map((todo: { id: string; title: string }) =>
<div key={todo.id}>{todo.id} {todo.title} </div>)}
</div>
</>
);
};
I use it in such a way that MyDivBlock is nested as a child of
const App: NextPage = () => {
return (
<div>
<Box >
<MyDivBlock key="key0" areaText="DIV1" another="another"/>
</Box>
</div>
)
}
Note that MyDivBlock is nested in Box and MyDivBlock has no ref attribute. This is important because I need to write Box code with no additional requirements for my nested children. And anyone who will use my Box should not think about constraints and ref attributes.
Then I need to get the dimensions of MyDivBlock in the code of Box component, and later attach some event listeners to it, such as scrolling. These dimensions and listeners will be used in the Box component. I wanted to use Ref to control it. That is, the Box will later observe changes in the dimensions and events of MyDivBlock by creating a ref-reference to them
I know that this kind of parent-child relationship architecture is implemented through forwardRef
And here is the Box code:
import React, { forwardRef, useImperativeHandle, useRef } from 'react';
export interface BoxProps extends React.ComponentProps<any> {
children?: Element[];
className: string;
}
export const Box: React.FC<BoxProps> = ({ children, ...rest }: BoxProps): JSX.Element => {
const childRef = useRef<HTMLDivElement>();
const ChildWithForwardRef = forwardRef<HTMLDivElement>((props, _ref) => {
const methods = {
show() {
if (childRef.current) {
console.log("childRef.current is present...");
React.Children.forEach(children, function (item) {
console.log(item)})
console.log("offsetWidth = " + childRef.current.offsetWidth);
} else {
console.log("childRef.current is UNDEFINED");
}
},
};
useImperativeHandle(_ref, () => (methods));
return <div ref={childRef}> {children} </div>
});
ChildWithForwardRef.displayName = 'ChildWithForwardRef';
return (
<div
className={'BoxArea'}>
<button name="ChildComp" onClick={() => childRef.current.show()}>get Width</button>
<ChildWithForwardRef ref={childRef} />
</div>
);
}
export default Box;
The result of pressing the button:
childRef.current is present...
[...]
$$typeof: Symbol(react.element) key: "key0" props: {areaText: 'DIV1', another: 'another'}
[...] Object
offsetWidth = undefined
As you can see from the output, the component is visible through the created ref. I can even make several nested ones and get the same for all of them.
But the problem is that I don't have access to the offsetWidth and other properties.
The other challenge is how can I add the addEventListener?
Because it works in pure Javascript with their objects like Element, Document, Window or any other object that supports events, and I have ReactChildren objects.
Plus I'm using NextJS and TypeScript.
Didn't dive too deep into the problem, but this may be because you are passing the same childRef to both div inside ChildWithForwardRef and to ChildWithForwardRef itself. The latter overwrites the former, so you have the method .show from useImperativeHandle available but not offsetWidth. A quick fix is to rewrite ChildWithForwardRef to use its own ref:
const ChildWithForwardRef = forwardRef<HTMLDivElement>((props, _ref) => {
const ref = useRef<HTMLDivElement>()
const methods = {
show() {
if (ref.current) {
console.log("ref.current is present...");
React.Children.forEach(children, (item) => console.log(item))
console.log("offsetWidth = " + ref.current.offsetWidth);
} else {
console.log("ref.current is UNDEFINED");
}
},
};
useImperativeHandle(_ref, () => (methods));
// Here ref instead of childRef
return <div ref={ref}> {children} </div>
});
But really I don't quite get why you would need ChildWithForwardRef at all. The code is basically equivalent to this simpler version:
const Box: React.FC<BoxProps> = ({ children, ...rest }: BoxProps): JSX.Element => {
const childRef = useRef<HTMLDivElement>();
const showWidth = () => {
if(childRef.current) {
console.log("childRef.current is present...");
React.Children.forEach(children, item => console.log(item))
console.log("offsetWidth = " + childRef.current.offsetWidth);
} else {
console.log("childRef.current is UNDEFINED");
}
}
return (
<div className={'BoxArea'}>
<button name="ChildComp" onClick={showWidth}>get Width</button>
<div ref={childRef}>{children}</div>
</div>
);
}
You can't solve this completely with React. I solved it by wrapping the child component, making it take the form of the parent.

react dynamically add children to jsx/component

Situation
I'm attempting to render a component based on template jsx pieces. Reason being because I have a few of these situations in my app but with subtle customizations and I'd rather leave the business logic for the customizations in the respective component, not the factory.
Example
Parent Template
<div></div>
Child Template
<p></p>
In the render function I want to add n child components to the parent. So if n=4 then I would expect output like
<div>
<p></p>
<p></p>
<p></p>
<p></p>
</div>
I tried using parentTemplate.children.push with no avail because the group template is still JSX at this point and not yet a rendered component. How can I accomplish this task using jsx + react ?
Extra
Here is what my actual code looks like so far
render() {
let groupTemplate = <ListGroup></ListGroup>
let itemTemplate = (item) => {
return <ListGroup.Item>{item.name}</ListGroup.Item>;
}
if (this.props.itemTemplate) itemTemplate = this.props.itemTemplate;
let itemsJsx;
this.props.navArray.forEach((item) => {
groupTemplate.children.push(itemTemplate(item))
});
return (
[groupTemplate]
);
}
From the parent:
const groupTemplate = <ListGroup className='custom-container-classes'></ListGroup>
const itemTemplate = <ListGroup.Item className='customized-classes'></ListGroup.Item>
<NavigationFactory groupTemplate={groupTemplate} itemTemplate={itemTemplate}>
</NavigationFactory>
You could use render props, which is the best way to achieve something like that:
The parent:
<NavigationFactory
itemTemplate={itemTemplate}
render={children => <ListGroup className='custom-container-classes'>
{children}
</ListGroup>}
/>
The Child:
render() {
this.props.render(this.props.navArray.map(item => <ListGroup.Item key={item.id}>
{item.name}
</ListGroup.Item>))
}
This will let you define the container on your calling function and enables you to create the chidlren as you want.
Hope this helps.
Unclear on your question but I have attempted to figure out based on your code on what you are trying to do, see if it helps, else I'll delete my answer.
The mechanism of below code working is that it takes in groupTemplate which in your case you'll be passing in as a prop which then takes children within it and returns them composed all together. There's default children group template which you can use if no related prop is passed and you have a mapper function within the render which determines which template to use and renders n number of times based on navArray length.
componentDidMount() {
// setup default templates
if (!this.props.navButtonContainerTemplate) {
this.prop.navButtonContainerTemplate = (items) => {
return <ListGroup>{items}</ListGroup>
}
}
if (!this.props.navButtonTemplate) {
this.props.navButtonTemplate = (item) => {
return (
<ListGroup.Item>
{item.name}
</ListGroup.Item>
)
}
}
}
render() {
const itemsJsx = this.props.navArray.map(obj => {
const itemJsx = this.props.navButtonTemplate(obj)
return itemJsx;
});
return this.props.navButtonContainerTemplate(itemsJsx);
}
Pass the contents as regular children to the ListGroup via standard JSX nesting:
render() {
let itemTemplate = (item) => {
return <ListGroup.Item>{item.name}</ListGroup.Item>;
}
if (this.props.itemTemplate) itemTemplate = this.props.itemTemplate;
return (
<ListGroup>
{this.props.navArray.map(item => (
itemTemplate(item)
))}
</ListGroup>
);
}

How can I get the cumulative width of a list of components?

I have a list of "selections" that are displayed using a component. I need to find the rendered width of all these selections. My template looks like this:
{props.selections.map((chip: SelectOptionType) => {
return (
<Chip text={chip.label} />
)
}
Typically, in a non-React application, I'd probably put a class on the <Chip /> and use jquery to select elements of that class name, then loop over them and just sum the widths together:
let sum: number = 0;
$(".someClassName").forEach(($el) => sum += $el.offsetWidth);
I know the suggested way of doing something similar to this is using refs, but it seems you cant create an array of refs. I tried doing something like this:
{props.selections.map((chip: SelectOptionType, index: number) => {
chipsRefs[index] = React.createRef<HTMLDivElement>();
return (
<div ref={chipsRefs[index]}>
<Chip text={chip.label} />
</div>
)
}
But as I quickly learned each Ref inside chipsRefs ended up with a null current.
Now I'm a bit at a loss for this and have tried finding examples of this use case but have come up empty.
Can you try this ?
ref={ref => {
chipsRefs[index] = ref
}}
Try doing something like this: https://codesandbox.io/s/awesome-haibt-zeb8m
import React from "react";
class Selections extends React.Component {
constructor(props) {
super(props);
this._nodes = new Map();
}
componentDidMount() {
this.checkNodes();
}
checkNodes = () => {
let totalWidth = 0;
Array.from(this._nodes.values())
.filter(node => node != null)
.forEach(node => {
totalWidth = totalWidth + node.offsetWidth;
});
console.log(totalWidth);
};
render() {
const { selections } = this.props;
return (
<div>
{selections.map((value, i) => (
<div key={i} ref={c => this._nodes.set(i, c)}>
{value}
</div>
))}
</div>
);
}
}
export default Selections;
The function we defined in the ref prop is executed at time of
render.
In the ref call-back function, ref={c => this._nodes.set(i, c)}
we pass in the index (i) provided by .map() and the html element
(c) that is provided by the ref prop, in this case the div itself.
this._nodes.set(i, c) will create a new key-value pair in our
this._nodes iterable, one pair for each div we created. Now we have recorded HTML elements (nodes) to work with that contain all the methods we need to calculate the totalWidth of your rendered list.
Lastly in checkNodes() we get the .offsetWidth of each node to get our totalWidth.

React function not working from child component

I am trying to get a function working which removes an image uploaded using React Dropzone and react-sortable.
I have the dropzone working, and the sort working, but for some reason the function I have on the sortable item which removes that particular item from the array does not work.
The onClick event does not seem to call the function.
My code is below.
const SortableItem = SortableElement(({value, sortIndex, onRemove}) =>
<li>{value.name} <a onClick={() => onRemove(sortIndex)}>Remove {value.name}</a></li>
);
const SortableList = SortableContainer(({items, onRemove}) => {
return (
<ul>
{items.map((image, index) => (
<SortableItem key={`item-${index}`} index={index} value={image} sortIndex={index} onRemove={onRemove} />
))}
</ul>
);
});
class renderDropzoneInput extends React.Component {
constructor (props) {
super(props)
this.state = { files: [] }
this.handleDrop = this.handleDrop.bind(this)
}
handleDrop (files) {
this.setState({
files
});
this.props.input.onChange(files)
}
remove (index){
var array = this.state.files
array.splice(index, 1)
this.setState({files: array })
this.props.input.onChange(array)
}
onSortEnd = ({oldIndex, newIndex}) => {
this.setState({
files: arrayMove(this.state.files, oldIndex, newIndex),
});
};
render () {
const {
input, placeholder,
meta: {touched, error}
} = this.props
return (
<div>
<Dropzone
{...input}
name={input.name}
onDrop={this.handleDrop}
>
<div>Drop your images here or click to open file picker</div>
</Dropzone>
{touched && error && <span>{error}</span>}
<SortableList items={this.state.files} onSortEnd={this.onSortEnd} onRemove={(index) => this.remove(index)} />
</div>
);
}
}
export default renderDropzoneInput
Update: This was caused by react-sortable-hoc swallowing click events. Setting a pressDelay prop on the element allowed the click function to fire.
This is old question, but some people, like me, who still see this issue, might want to read this: https://github.com/clauderic/react-sortable-hoc/issues/111#issuecomment-272746004
Issue is that sortable-hoc swallows onClick events as Matt found out. But we can have workarounds by setting pressDelay or distance.
For me the best option was to set minimum distance for sortable list and it worked nicely
You can also use the distance prop to set a minimum distance to be dragged before sorting is triggered (for instance, you could set a distance of 1px like so: distance={1})

Resources