recently I have been using react-virtualized library to render my tree item view. I have followed example from the docs however I end up having very strange problem with items disappearing when I scroll down.
I have created codesandbox to show this behaviour and code.
https://codesandbox.io/s/bitter-snow-23vci?file=/src/App.js
Main idea of virtualized list to render it as a list.
If you pass down tree like structure and render it like in your code sample
<List
....
rowCount={data.length}
/>
You don't change rowCount value and keep expanded state in your Node component.
const Node = ({ data, listRef, depth }) => {
const [isExpanded, setIsExpanded] = React.useState(false);
But then you scroll out of screen your Node element will be destroyed and recreated then you return.
You need to keep your selections outside of Node element.
like
// [key]: value structure there key is id of element and value [true, false].
const rootObject = {[elementId]: true};
const App = () => {
const [visibleNodes, setVisibleNodes] = useState(rootObject)
....
<List
...
rowRenderer={({ index, style, key }) => {
return (
<Node
setVisibleNodes={setVisibleNodes}
visibleNodes={visibleNodes}
style={style}
key={key}
data={data[index]}
listRef={ref}
depth={1}
/>
);
}}
rowCount={data.length}
width={width}
/>
And in Node
const Node = ({ data, listRef, depth, setVisibleNodes, visibleNodes }) => {
const isExpanded = visibleNodes[data.id];
const handleClick = (e) => {
if (data.children.length === 0) return;
e.stopPropagation();
setVisibleNodes({...visibleNodes, [data.id]: !!isExpanded});
listRef.current.recomputeRowHeights();
listRef.current.forceUpdate();
};
return (
<div onClick={handleClick}>
{data.children.length ? (isExpanded ? "[-]" : "[+]") : ""} {data.name}
{isExpanded && (
<div style={{ marginLeft: depth * 15 }}>
{data.children.map((child, index) => (
<Node
key={index}
data={child}
listRef={listRef}
depth={depth + 1}
/>
))}
</div>
)}
</div>
);
};
I think it works)
But it's better to do such things like real list and make tree hierarchy just visually. By that way you'll use Virtualisation List as it was purposed by creators)
Related
I am using react-transition-group to fade out various components. I'm converting simple conditional renders such as:
{valueToDisplay && <MyComponent {...valueToDisplay} />}
To transitions such as:
<CSSTransition
in={!!valueToDisplay}
unmountOnExit
classNames="fade"
addEndListener={(node, done) => node.addEventListener("transitionend", done, false)}
>
<MyComponent {...valueToDisplay} />
</CSSTransition>
The issue I'm running into is when the "in" property of the transition becomes false, and the exit transition is running, the child component will now have null prop values. This can cause exceptions or cause the child content to flash and change during the exit. What I would like to see instead is that during the exit transition, the content will remain unchanged.
The first solution I came up with was to make child components to cache previous values of their props, and then use those previous values when their props become null. However I don't like this solution because it forces all components which will be transitioned to introduce new and confusing internal logic.
The second attempt I made was to create a wrapper component which cached the previous value of props.children, and whenever "in" becomes false, renders the cached children instead. This essentially "freezes" the children as they were the last time in was true, and they don't change during the exit transition. (If this solution is the general practice, is there a better way of doing this, perhaps with the useMemo hook?)
For such a common use case of fading content out, this solution doesn't seem very intuitive. I can't help but feeling I'm going about this the wrong way. I can't really find any examples of having to cache/memoize content to keep it displaying during fade outs. It seems like something somewhere has to remember the values to display when performing the exit transition. What am I missing?
Here is a minimal example and working example:
import { useRef, useEffect, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { CSSTransition } from 'react-transition-group';
const Pet = ({ type, age }) => {
return (
<div>
Your pet {type || 'null'} is age {age || 'null'}
</div>
);
};
const Fade = ({ show, children }) => {
const nodeRef = useRef(null);
return (
<CSSTransition
nodeRef={nodeRef}
in={show}
unmountOnExit
classNames="fade"
addEndListener={(done) => nodeRef.current.addEventListener("transitionend", done, false)}
>
<span ref={nodeRef}>
{children}
</span>
</CSSTransition>
);
};
const FadeWithMemo = ({ show, children }) => {
const previousChildren = useRef();
useEffect(() => {
previousChildren.current = show ? children : null;
}, [show, children]);
return (
<Fade show={show}>
{show ? children : previousChildren.current}
</Fade>
);
};
const Example = () => {
const [currentPet, setCurrentPet] = useState(null);
const getPet = () => {
return {
type: (Math.random() > .5) ? 'Cat' : 'Dog',
age: Math.floor(Math.random() * 15) + 1
};
};
return (
<>
<button onClick={() => setCurrentPet(getPet())}>Set</button>
<button onClick={() => setCurrentPet(null)}>Clear</button>
<div>
The Problem:
<Fade show={!!currentPet}>
<Pet {...currentPet} />
</Fade>
</div>
<div>
Potential Fix:
<FadeWithMemo show={!!currentPet}>
<Pet {...currentPet} />
</FadeWithMemo>
</div>
</>
);
};
const root = createRoot(document.getElementById('root'));
root.render(<Example />);
You can detach the visible condition from the pet state so that you have more granular control over whether something is visible and what is actually being displayed.
const Example = () => {
const [currentPet, setCurrentPet] = useState(null);
const [showPet, setShowPet] = useState(false);
const getPet = () => {
return {
type: (Math.random() > .5) ? 'Cat' : 'Dog',
age: Math.floor(Math.random() * 15) + 1
};
};
return (
<>
<button onClick={() => {
setCurrentPet(getPet());
setShowPet(true);
}}>Set</button>
<button onClick={() => setShowPet(false)}>Clear</button>
<div>
<Fade show={showPet}>
<Pet {...currentPet} />
</Fade>
</div>
</>
);
};
or you can have the visible be part of the pet state and only set that part to false.
I've been trying to do a sort of toggle whereby you are able to click on a question to expand the answer. I've tried adapting the code from https://codesandbox.io/s/polished-rain-xnez0?file=/src/App.js, which was from another question on here.
Instead of creating a map in the parent component and passing in them separately to a reusable 'Expandable' component to render separate functional components as shown in the example, I tried creating the map within the FAQ component:
FAQ Expandable Component:
const FAQ = ({ questions }) => {
const [expanded, setExpanded] = useState(false);
const handleClick = () => {
setExpanded((prevExpanded) => !prevExpanded);
};
const renderedQuestions = questions.map((question, index) => {
return (
<React.Fragment key={question.id}>
<FAQIndividualWrapper>
<FAQTitle
className='title'
onClick={() => handleClick()}
>
{/* <i></i> */}
{question.title}
</FAQTitle>
<FAQContent className='content' style={{ display: expanded ? "block" : "none" }}>
{question.content}
</FAQContent>
</FAQIndividualWrapper>
</React.Fragment>
)
})
return (
<>
{renderedQuestions}
</>
)
Parent Component:
const questions = [
{
id: 1,
title: 'Question 1',
content: 'Answer 1'
},
{
id: 2,
title: 'Question 2',
content: 'Answer 2'
}
]
const FAQSection = () => {
return (
<FAQPageContainer>
<FAQWrapper>
<FAQ questions={questions} />
</FAQWrapper>
</FAQPageContainer>
)
}
However, my code results in all the answers being expanded on any click of either question. Why is this happening?
Also, how should I structure and fix the code for 'ideal' programming?
Thank you!
The problem with your code is that you only have one expanded state for all questions. This means the open/collapse state is the same for all questions.
If you want to include all of your code inside one component. You need to somehow distinguish the open/collapse state of each child.
Here I'm using an array to store individual open/collapse state of each child.
// initial state is an array with the length of your questions array, default to false
const [expandedIndexes, setExpandedIndexes] = useState(
Array(info.length).fill(false)
);
const handleClick = (index) => {
setExpandedIndexes((prevExpandedIndexes) => {
const newState = [...prevExpandedIndexes];
// set state for the corresponding index
newState.splice(index, 1, !prevExpandedIndexes[index]);
return newState;
});
};
return (
<div className="details">
{info.map(({ title, details, id }, index) => (
<div key={id} className="details-wrapper">
<div>
<h3 className="title">{title}</h3>
<button onClick={() => handleClick(index)}>+</button>
</div>
<p
className="text"
// check the corresponding state to display
style={{ display: expandedIndexes[index] ? "block" : "none" }}
>
{details}
</p>
</div>
))}
</div>
);
Codesandbox
Also, how should I structure and fix the code for 'ideal' programming?
Ideally, you want to follow the convention in your codesandbox link above. That way you don't need to deal with this kind of logic, each child will have its own state/handleClick function.
It is correct that the issue is caused by the fact that you've only a single boolean state.
Use an object to store the ids of the questions that are expanded, toggling a boolean value for each.
Example:
const FAQ = ({ questions }) => {
const [expanded, setExpanded] = useState({});
// Curried callback to enclose the id in callback scope
const handleClick = id => () => {
setExpanded(expanded => ({
...expanded,
[id]: !expanded[id],
}));
};
return questions.map((question) => (
<FAQIndividualWrapper key={question.id}>
<FAQTitle
className='title'
onClick={handleClick(question.id)}
>
{question.title}
</FAQTitle>
<FAQContent
className='content'
style={{ display: expanded[question.id] ? "block" : "none" }}
>
{question.content}
</FAQContent>
</FAQIndividualWrapper>
));
}
Libraries like react-virtualized, react-window and react-virtuoso have item count property like in code below from materal-ui. However it is located within return. Is there any way to make item counterupdatable?
export default function VirtualizedList() {
const classes = useStyles();
return (
<div className={classes.root}>
<FixedSizeList height={400} width={300} itemSize={46} itemCount={200}>
{renderRow}
</FixedSizeList>
</div>
);
}
Yes you can pass on a dynamic value to the itemCount property in FixedSizeList. It take care of it and also ensure that the scroll remain where it is currently
A sample code would look like
const Example = () => {
const [rowCount, setRowCount] = useState(10);
useEffect(() => {
setTimeout(() => {
console.log("changed");
setRowCount(1000);
}, 10000);
}, []);
console.log(rowCount);
return (
<List
className="List"
height={150}
itemCount={rowCount}
itemSize={35}
width={300}
>
{Row}
</List>
);
};
Working demo
I am struggling on how to properly set up a long list of Menu Items on a Select control that uses sticky SubHeaders. The problem is that when the items scroll they obscure the subheaders.
I looked at the Material UI examples of grouped Select items as a start. I wanted behavior that looked like the Material UI example with pinned subHeader Lists.
Here is a codeSandbox of what I'm trying.
Below is a snippet of my code:
<Select
className={classes.root}
MenuProps={{ className: classes.menu }}
value="Pick one"
onChange={e => {}}
>
{subHeaders.map(header => (
<li key={header}>
<ul>
<ListSubheader>{header}</ListSubheader>
{items.map(item => (
<MenuItem key={item} value={item}>
{item}
</MenuItem>
))}
</ul>
</li>
))}
</Select>
Here is a snapshot of the problem:
Using the Select component we can even reproduce the behavior with some corrections. But it won't work for you. The Select component does not expect items nested within your child's elements. That way, we will never be able to identify the element that is selected.
Alternatively, we have the Autocomplete component.
It can better supply what you need.
Regarding the example you provided, we can do something, but again, we will not be able to maintain the state of the selected item.
To achieve the same behavior as the list, we need to apply the same behavior to the list that the Menu will render.
Select will render a Menu that inherits List, so we can apply the same behavior as the list example through the prop MenuListProps property.
I applied the fixes to your example
I hope it helps.
I managed to make a working solution of Material-ui select with and sticky MenuItems.
use MaterialUI MenuItem instead of all the <li> <ul> <ListSubheader>
const [isOpen, setIsOpen] = useState(false);
const [value, setValue] = useState();
const onToggle = () => {
setIsOpen((prev) => !prev);
};
const onClose = () => {
setIsOpen(false);
};
const _onChange = (event: React.ChangeEvent<{ value: unknown }>) => {
const valueToSelect = event.target.value as Value;
if (
isResetSeletced(valueToSelect) ||
(multiple
? !valueToSelect.length ||
valueToSelect.length < minSelections ||
(valueToSelect as string[]).some((option) => !option)
: !valueToSelect?.length && minSelections > 0)
) {
return;
}
event.persist();
onChange(valueToSelect);
};
const renderValue = (selected: any) => {
if (!selected.length) {
return '';
}
if (multiple) {
const isReachedLimit = selected.length > MAX_SELECTIONS;
const hiddenTags = isReachedLimit ? (
<span>+{value.length - MAX_SELECTIONS}</span>
) : null;
const selectionsToShow = isReachedLimit
? selected.slice(0, MAX_SELECTIONS)
: selected;
return (
<StyledTagsContainer>
<Tags values={selectionsToShow} onRemoveTag={onRemoveTag} />
{hiddenTags}
</StyledTagsContainer>
);
}
return selected;
};
const resetMenuItem = secondaryOptions?.map((resetItem, index) => {
return (
<MenuItem
key={resetItem.value + index}
onClick={() => {
resetItem.onClick();
}}
isLast={!index}
isSelected={
resetItem.value === resetSelected?.value ||
resetItem.value === value ||
(multiple && resetItem.value === value[0])
}
value={resetItem.value}
icon={<RadioIcon />}
>
{resetItem.text}
</MenuItem>
);
});
<Select
displayEmpty
onClose={onClose}
value={value}
onChange={_onChange}
renderValue={renderValue}
open={isOpen}
>
{menuItems}
<div style={{ position: 'sticky', bottom: 0 }}>
{resetMenuItem}
</div>
</Select>
I have the following piece of code for my component. The desired behaviour for the button is to change the className for each li, but this is not working.
const Booking = (props) => {
let { hidden } = useContext(ContextBooking)
let completed = props.completed
return (
<li
className={ //should change according to the button click below
completed && hidden ?
'booking-complete hide'
: completed ?
'booking-complete'
:
'bookings'
}}
key={props.id}
id={props.id}
>
<h3>{props.date}</h3>
<h4>{props.time}</h4>
<h5>{props.name}</h5>
</li>
)
}
{!completed && (
<button
onClick={() => {
if (!completed && !hidden) {
completed = !completed //does make it false
hidden = !hidden //does make it false
} //above works, but won't change classname for each 'li'
else if (completed && hidden) {
completed = !completed
hidden = !hidden
}
}}>
Complete
</button>
)}
In another component, I am creating multiple of these 'Booking' components, by filling in the details with info that come from a json file
const DisplayBookings = () => {
const display = (day) => allBookings.map(item => //allBookings is a json file
item.day === day &&
<Booking
completed={item.completed}
key={item.id}
id={item.id}
time={item.time}
name={item.name}
date={item.date}
/>
)
I emphasised json file as I believe it could be the source of the problem?
A component can in most cases not update its own props, and doing so even if possible is an antipattern.
You can instead use state for updating the components state.
You can create hooks for setting state like this:
const [isCompleted, setIsCompleted] = useState(props.completed);
const [isHidden, setIsHidden] = useState(hidden);
Then in your onClick you use this to update the values:
setIsCompleted(!isCompleted);
setIsHidden(!isHidden);