Elements doesn't survive state change for css transitions in React - javascript

I have a highscore list that updates automatically with current scores once every minute.
It then updates the component with this.setState({ data: newData });.
Below I sort the new data by score, run each item through map that updates their style.top.
Javascript
...
let current = this.state.data.sort((a,b) => {
if(a.score < b.score) return -1;
if(a.score > b.score) return 1;
return 0;
});
let items = current.map((item,i) => {
return (
<div
key={item.name}
className="item"
style={{ top: (i*30) + 'px', backgroundColor: item.color }}>
{item.name}
</div>
);
});
return (
<div>
{items}
</div>
);
CSS
.item {
position: absolute;
transition: top 1s;
}
I want the items to animate up and down to their new positions, but this doesn't happen. The browser seems to remember parts of the DOM, but not others.
React seems to understand which item is which by using the unique key, if I inspect using React Developer Tools, but the actual DOM doesn't.
Even stranger, only items moving up the list (higher score) seems to be remembered. Items moving down is recreated, and therefor the transition doesn't work. They just pop into existence without transitioning to their new position.
Here is a working example to demonstrate. (JSFiddle)

If you change it so that the items are not reordered between render cycles, but are always rendered in the same order with different top styles, it animates correctly. I think this is because changing the order of elements forces them to be removed from the DOM and re-added by react.
The relevant code is:
render() {
let current = this.state.data.slice().sort((a,b) => {
if(a.score < b.score) return -1;
if(a.score > b.score) return 1;
return 0;
});
let items = this.state.data.map(item => {
let position = current.indexOf(item);
return (
<div
key={item.name}
className="item"
style={{ top: (position*30) + 'px', backgroundColor: item.color }}>
{item.name}
</div>
);
});
return (
<div>
{items}
</div>
);
}
Updated demo

React redraws the DOM elements you update. This will resolve in really odd behaviours, such as blinking, jumping etc.
The React team offered a couple of solutions for this before they started working on Fiber instead. With the release of Fiber I hope we'll see better support for transitions and animations.
Until that time, I recommend you use external libs for animations such as:
https://github.com/FormidableLabs/react-animations For general animation.
And https://github.com/reactjs/react-transition-group for elements entering and exiting the DOM.

Related

Changing content dynamically inside react-native-pager-view (or any other)

i'm working on a project where i'm going to be displaying details and information about a certain book page by page inside a pager view as page components, the book contains 500+ pages so i can't just create 500 page components like that and insert them into the pager..what i thought is i can get a specific page, render its component, alongside the previous, and the next page only..and when the user swipes to the next/previous page i would change the component state, and have it re-render with the new 3 pages, the current one, the previous, and the next one. the logic in my head makes perfect sense, but it just won't work when i try to apply it.
can anyone help me, guide me to certain videos that explain this principal more? i feel like i'm missing something.
the code goes like this:
first i have the PagesContainer, here i will create the PagesDetails component(s) based on the current page, and having these pages in react-native-pager-view (you can suggest me a better option). for testing purpose only, i set the swipe end callback (onPageSelected) to increment the current page number state, which would then cause the component to re-render and create the new page component(s), that happens only when the user swipes to new page of course:
function PagesContainer({ currentPageNumber, setCurrentPageNumber }) {
const [pageComponents, setPageComponents] = useState([]);
useEffect(() => {
let compArr = [];
compArr.push(<PageDetails key="current" pageNumber={currentPageNumber} />);
if (currentPageNumber > 1) {
compArr.unshift(<PageDetails key="previous" pageNumber={currentPageNumber - 1} />)
}
if (currentPageNumber <= 500) {
compArr.push(<PageDetails key="next" pageNumber={currentPageNumber + 1} />)
}
setPageComponents(compArr);
}, [currentPageNumber])
return (<PagerView style={{ flex: 1 }}
initialPage={currentPageNumber == 1 ? 0 : 1}
layoutDirection={"rtl"}
onPageSelected={(PageSelectedEvent)=>{setCurrentPageNumber(currentPageNumber + 1)}}
>
{pageComponents.map(page => {
return page;
})}
</PagerView>)
}
and then here i have my PageDeatails component where i simply display texts and details of the page, i take the data from the bookData object which is imported at the very top of the code file:
function PageDetails({ pageNumber }) {
const [pageContent, setPageContent] = useState(null);
useEffect(() => {
setPageContent(bookData[pageNumber]["pageContent"]);
}, []);
return (
<View>
{pageContent && <View>
{pageContent.map(item => {
return (<Text>item</Text>)
})}
</View>
}
</View>
)
}
The logic makes perfect sense in my head but it just doesn't work when i test it..what am i missing? what am i doing wrong?
use the PagerView reference using useRef also store the page index and pass to initialPage get current page index from onPageSelected callback
like:
initialPage={currentPageIndex}

Cells overlapping problems in react virtualized with cell measurer

I am using React Virtualized to window my items list which has the following functionality
1) onClicking on any item, the user will be prompted with a details panel where he will be able to update the details of the item. And when he goes back to the list, he will be able to see the details in the item cell in the list. He can add a lot of details to make it bigger or reduce the size by removing the details
2) He can delete the item, or add another item to the list with certain details or without details.
CellMeasurer serves my requirement as dynamic height is supported. But I am having following issues with it
1) initially when my list mounts for the first time, first few items are measured and shown correctly but as soon as I scroll to the end, items get overlapped with each other.(positining isnt correct, I am guessing the defaultHeight is being applied to the unmeasured cells). This works fine as soon as the list is rerendered again.
2) Also, when I am updating the details of an item the list doesnt update with the new height adjustments
I am sure that somewhere my implementation is incorrect but have spent a lot of time hammering my head for it. Please let me know what can be done here to fix this
ItemView = props => {
const {index, isScrolling, key, style} = props,
items = this.getFormattedItems()[index]
return (
<CellMeasurer
cache={this._cache}
columnIndex={0}
isScrolling={isScrolling}
key={key}
rowIndex={index}
parent={parent}>
{({measure, registerChild}) => (
<div key={key} style={style} onLoad={measure}>
<div
onLoad={measure}
ref={registerChild}
{...parentProps}>
<Item
onLoad={measure}
key={annotation.getId()}
{...childProps}
/>
</div>
</div>
)}
</CellMeasurer>
)
}
renderDynamicListOfItems(){
return (<div>
<List
height={500}
ref={listComponent => (_this.listComponent = listComponent)}
style={{
width: '100%'
}}
onRowsRendered={()=>{}}
width={1000}
rowCount={props.items.length}
rowHeight={this._cache.rowHeight}
rowRenderer={memoize(this.ItemView)}
// onScroll={onChildScroll}
className={listClassName}
overscanRowCount={0}
/>
</div>
)
}
Also, I am manually triggering the remeasurement of my item in its componentDidUpdate like follows()
Component Item
...
componentDidUpdate() {
console.log('loading called for ', this.props.annotation.getId())
this.props.onLoad()
}
...
In the main parent I am recomputing the heights of the list every time the list has updated and triggering a forceupdate as follows
Component ParentList
...
componentDidUpdate() {
console.log("calling this parent recomputing")
this.listComponent.recomputeRowHeights()
this.listComponent.forceUpdateGrid()
}
...
I just faced the same issue and it turned out to be some layout updates in my Item component, that changed the height of the item after it has been measured by the CellMeasurer.
Fix was to pass down the measure function and call it on every layout update.
In my case it was
images being loaded
and text being manipulated with Twemoji (which replaces the emojis with images).
I also had problems getting the correct height from the cache after prepending some new list items when scrolling to the top of the list. This could be fixed by providing a keyMapper to the CellMeasurerCache:
const [cache, setCache] = useState<CellMeasurerCache>();
useEffect(() => {
setCache(new CellMeasurerCache({
keyMapper: (rowIndex: number, columnIndex: number) => listItems[rowIndex].id,
defaultHeight: 100,
fixedWidth: true,
}));
}, [listItems]);

React Prop Becomes Undefined on Re-render

so I'm currently working on learning react.js, and I've run into an issue I haven't been able to move past.
So, the broad stroke is that I have a container which is meant to render a grid of images. If you select one of the images, you'll be able to change it to another image from a list of options.
Here is the potentially relevant portion of the Grid container which is rendering fine at this moment. If the full code in context helps, you can find it here: https://codepen.io/KrisSM/pen/wvvmoqg
_onBrigadePosSelection = slot => {
console.log("This was the division selected in Brigade Grid: " + slot);
this.props.onBrigadePosSelected(slot);
};
render() {
for (let i = 0; i < 15; i++) {
//each block is a separated so that they can be rendered as different rows in the return
if (i <= 4) {
rowOne.push(
<div key={i}>
<ImageButton
btnType={"Grid"}
imageSrc={this.props.icons[this.props.brigadeDesign[i]]}
clicked={() => this._onBrigadePosSelection(i)}
/>
</div>
);
}
if (i > 4 && i <= 9) {
rowTwo.push(
<div key={i}>
<ImageButton
btnType={"Grid"}
imageSrc={this.props.icons[this.props.brigadeDesign[i]]}
clicked={() => this._onBrigadePosSelection(i)}
/>
</div>
);
}
if (i > 9 && i <= 14) {
rowThree.push(
<div key={i}>
<ImageButton
btnType={"Grid"}
imageSrc={this.props.icons[this.props.brigadeDesign[i]]}
clicked={() => this._onBrigadePosSelection(i)}
/>
</div>
);
}
}
So, when an image button is selected, it fires the onBrigadePosSelection function, which returns the selected button to the container for the grid where this function is then hit.
onBrigadePosSelected = slot => {
this.setState({ selectedDivision: slot });
console.log("This is the selected division: " + slot);
};
So, thus far, everything works. The grid renders and when a button is hit, this console log correctly states which button was hit. But this is where things start to get odd. When the state changes, their is a re-render of course, but after that re-render, selectedDivision becomes undefined.
Looking at your codes, I think the problem comes from these lines:
clicked={() => this._onBrigadePosSelection(i)}
The reason is that i will keep changing for each iteration, but () => this._onBrigadePosSelection(i) is dynamically called function.
It means that whenever you click the button, it'll get the latest value of i (or garbage or undefined) because i was garbage collected already.
I would suggest you pass the function this._onBridgePosSelection as props and call it inside the <ImageButton /> component instead.
<ImageButton
btnType={"Grid"}
imageSrc={this.props.icons[this.props.brigadeDesign[i]]}
index={i}
onBrigadePosSelection={this._onBrigadePosSelection}
/>
Then inside <ImageButton /> component, you can call it with this:
this.props.onBrigadePosSelection(this.props.index)

Enlarging single item in React Native ScrollView

I have a ScrollView like so:
<ScrollView>
{this.state.businessMerchants.map(merchant => (
<Business
merchant={merchant}
key={merchant.id}
/>
))}
</ScrollView>
Usually with 2-4 items in it.
I want to highlight the currently top item and make it take up more room (maybe 10% more?). This "top item" would switch as one scrolls through the list.
Is there a more standardized way of doing this, or will I have to do something like use scrollEventThrottle and a custom function like so?
Pseudocode below:
if((findHeightOfItem + padding) * multiple === itemPos) {
addSizeHere()
}
Quite curious about what's the best/most performative way of doing this in React Native.
Steps:
Decide the height of each component itemHeight
Apply onScroll and get current scrolled height scrolledHeight
Have one local state called currentItemIndex which will be decided in onScroll function and will be calculated as Math.floor(scrolledHeight / itemHeight)
Send isCurrentItem as prop to Business component
Apply necessary styles to Business component depending on value of boolean isCurrentItem
I would suggest you to use Animated view with transform scale
handleScroll = (e) => {
const currentHeight = e.nativeEvent.contentOffset.y;
const currentItemIndex = Math.floor(currentHeight / 100);
this.setState({ currentItemIndex });
}
<ScrollView onScroll>
{this.state.businessMerchants.map((merchant, index) => (
<Business
merchant={merchant}
isCurrentItem={index === this.state.currentItemIndex}
key={merchant.id}
/>
))}
</ScrollView>

React-Virtualized List skips rows when scrolling

I'm having an issue when I try to render a list of items as follows:
render()
{
const data = (my data);
const cellRenderer = ({index, key}) => (
<li key={key}>
<Datum data={data[index]} />
</li>
);
return (
<AutoSizer>
{
({ width, height }) => (
<CellMeasurer cellRenderer={ cellRenderer }
columnCount={ 1 }
width={ width }
rowCount={ transactions.length }>
{
({ getRowHeight }) => (
<List height={ height }
overscanRowCount={ 50 }
noRowsRenderer={ () => (<div> Nix! </div>) }
rowCount={ transactions.length }
rowHeight={ getRowHeight }
rowRenderer={ cellRenderer }
width={ width } />
)
}
</CellMeasurer>
)
}
</AutoSizer>
);
}
Now when I scroll down it skips every second list-item, until I end up with half the page empty but still scrollable.
When scrolling up it's even worse, it skips half the page.
AutoSizerand CellMeasurer seem to be working fine. I stepped through the code a bit, and it looks like they do create the correct measurement.
My data is just a static array of objects. Not a promise or stream.
I also changed my Datum component a few times to make sure it's completely static markup but that didn't change anything.
Any ideas anybody?
[edit]
Here's a Plunker showing my problem: https://plnkr.co/edit/2YJnAt?p=preview
Ironically, while fiddling with it, I accidentally figured out what I had done wrong. I'll submit an answer with my solution.
Right, I found the problem (and so did #MBrevda! +1!)
The rowRenderer method takes a style that needs to be applied to the rendered list element: https://plnkr.co/edit/FzPwLv?p=preview

Categories

Resources