React ref assignment inconsistencies when using a mapping function? - reactjs

I'm setting up a virtualized scrolling component in React, and using refs with a recycled observer to notify the app when to prepare another batch of data. Inside my Grid component, I map over the current batch of data and assign a ref to a sentinel div, except that ref returns null in componentDidMount(). I don't understand why since componentDidMount fires after render executes, so the reference should be available.
The only workaround to this I've found is using this janky solution in my componentDidMount: setTimeout(() => this.observer.observe(this.targetRef.current), 0);.
import React, { Component, createRef } from "react";
class Grid extends Component {
constructor(props) {
super(props);
this.state = {
batch: []
};
this.observer = null;
this.targetRef = null;
this.lastRowFirstVisible =
props.rowCount * props.columnCount - props.columnCount;
this.config = {
rootMargin: "0px",
threshold: 1
};
this.setTargetRef = element => {
this.targetRef = element;
};
}
componentDidMount() {
const { startIndex, numberToDisplay } = this.props;
this.setBatch(startIndex, numberToDisplay);
this.observer = new IntersectionObserver(function(entries, self) {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log(entry);
// self.unobserve(entry.target);
}
});
}, this.config);
setTimeout(() => this.observer.observe(this.targetRef), 0);
}
setBatch = (startIndex, numberToDisplay) => {
const batch = this.getBatch(startIndex, numberToDisplay);
this.setState({ batch });
};
getBatch = (startIndex, numberToDisplay) => {
const { data } = this.props;
return data.slice(startIndex, numberToDisplay);
};
// TO DO
updateObserver = () => {
this.observer.observe(this.targetRef.current);
};
render() {
const { lastRowFirstVisible } = this;
const { batch } = this.state;
const { elementWidth, elementHeight } = this.props;
console.log(lastRowFirstVisible);
return (
<>
{batch.map((element, localIndex) => {
const { index } = element;
console.log(localIndex === lastRowFirstVisible);
return localIndex === lastRowFirstVisible ? (
<div
id={index}
key={index}
style={{ width: elementWidth, height: elementHeight }}
className="card"
ref={this.setTargetRef}
>
{this.props.renderRow(element)}
</div>
) : (
<div
key={index}
style={{ width: elementWidth, height: elementHeight }}
className="card"
>
{this.props.renderRow(element)}
</div>
);
})}
</>
);
}
}
export default Grid;
Expected results: render function finishes executing, assigns DOM node to this.targetRef for use in componentDidMount()
Actual results: this.targetRef is still null in componentDidMount()

Related

RecyclerListView scrolls to top onEndReached with functional component

I have implemented a RecyclerListView(Flipkart Github) using Redux as seen below. Everything seems to be working great except when onEndReached is called and a new batch of data comes through, the list gets positioned to the top of the page rather than remaining smooth. See that behavior in the GIF below:
Note: This is happening on web(chrome). I tried the latest stable and 2.0.13-alpha.1
import React, { useCallback } from 'react';
import { View, Text, Dimensions } from 'react-native';
import { useSelector, useDispatch } from 'react-redux';
import { RecyclerListView, DataProvider, LayoutProvider } from 'recyclerlistview/web';
import { createSelector } from 'reselect';
import { loadData } from '../actions';
const selectData = createSelector(
state => state.data,
data => Object.values(data),
);
let containerCount = 0;
class CellContainer extends React.Component {
constructor(args) {
super(args);
this._containerId = containerCount++;
}
render() {
return (
<View {...this.props}>
{this.props.children}
<Text>Cell Id: {this._containerId}</Text>
</View>
);
}
}
const List = ({ isServer }) => {
let { width } = Dimensions.get('window');
const dispatch = useDispatch();
const data = useSelector(selectData);
const dataDataProvider = new DataProvider((r1, r2) => {
return r1 !== r2;
}).cloneWithRows(data);
const onEndReached = useCallback(() => dispatch(loadData()), [dispatch]);
const layoutProvider = new LayoutProvider(
() => 0,
(type, dim) => {
dim.width = width;
dim.height = 240;
},
);
const rowRenderer = (type, data) => {
return <CellContainer />;
};
return (
<View
style={{
display: "flex",
flex: 1,
width: "100vw",
height: "100vh"
}}
>
<RecyclerListView
layoutProvider={layoutProvider}
dataProvider={dataDataProvider}
onEndReached={onEndReached}
rowRenderer={rowRenderer}
/>
</View>
);
};
export default List;
UPDATE: I can confirm that the class-based version works with no issues using redux connect. Leaning towards this being some kind of incompatibility with the library. Interesting nonetheless.
Below snippet is a simplified working example of this demo https://codesandbox.io/s/k54j2zx977
class App extends React.Component {
constructor(props) {
let { width } = Dimensions.get('window');
super(props);
this.state = {
dataProvider: new DataProvider((r1, r2) => {
return r1 !== r2;
}),
layoutProvider: new LayoutProvider(
index => 0,
(type, dim) => {
dim.width = width;
dim.height = 240;
},
),
count: 0,
};
}
componentDidUpdate(prevProps) {
if (this.props.data.length !== prevProps.data.length) {
this.setState({
dataProvider: this.state.dataProvider.cloneWithRows(this.props.data),
count: this.props.data.length
});
}
}
componentWillMount() {
this.props.loadData();
}
async fetchMoreData() {
this.props.loadData();
}
rowRenderer = (type, data) => {
//We have only one view type so not checks are needed here
return <CellContainer />;
};
handleListEnd = () => {
this.fetchMoreData();
//This is necessary to ensure that activity indicator inside footer gets rendered. This is required given the implementation I have done in this sample
this.setState({});
};
render() {
//Only render RLV once you have the data
return (
<View style={styles.container}>
{this.state.count > 0 ? (
<RecyclerListView
style={{ flex: 1 }}
contentContainerStyle={{ margin: 3 }}
onEndReached={this.handleListEnd}
dataProvider={this.state.dataProvider}
layoutProvider={this.state.layoutProvider}
renderAheadOffset={0}
rowRenderer={this.rowRenderer}
/>
) : null}
</View>
);
}
}
export default connect(
state => ({
data: selectData(state),
}),
dispatch => bindActionCreators({ loadData }, dispatch),
)(App);
Just add the below line of code after your layoutProvider. The shouldRefreshWithAnchoring property of layoutProvider prevents recycler view list to not refresh the existing rows if more rows are added to the list.
layoutProvider.shouldRefreshWithAnchoring = false;
Solution is here:
https://github.com/Flipkart/recyclerlistview/issues/449
Just add
const [layoutProvider] = React.useState(
new LayoutProvider(
(index) => 1,
(type, dim) => {
dim.width = ITEM_HEIGHT
dim.height = ITEM_HEIGHT
}
)
)

How to stop react component from infinite re-render

I wanna use js-cookie in my app, but the time I get the cookie my component keeps re-rendering until the browser crash.
The error I get is: setState(...): Cannot update during an existing state transition ...
I've just used the shouldComponentUpdate but it caused the click events not working.
shouldComponentUpdate(nextProps, nextState)
{
return nextState.language != this.state.language;
}
Does anybody know any other solution rather than shouldComponentUpdate to stop a component from infinite re-render?
class MainLayout extends Component {
constructor(props) {
super(props);
console.log('constructor');
this.state = {
sideBarOpen: false,
languages: getStoreLanguages,
language: Cookies.get('langCode')
}
}
componentWillMount() {
this.props.langCode();
this.props.defaultLangCode();
}
componentDidMount() {
$('.dropdown-toggle').megaMenu && $('.dropdown-toggle').megaMenu({ container: '.mmd' });
}
shouldComponentUpdate(nextProps, nextState) {
return nextState.language != this.state.language;
}
toggleSidebar = () => {
this.setState({
sideBarOpen: !this.state.sideBarOpen,
});
}
overlayClickHandler = () => {
this.setState({
sideBarOpen: false,
});
}
handleLanguage = (langCode) => {
if (Cookies.get('langCode')) {
return Cookies.get('langCode');
} else {
Cookies.set('langCode', langCode, { expires: 7 });
return langCode;
}
}
render() {
let overlay = { display: this.state.sideBarOpen ? 'block' : 'none' };
const langCode = this.handleLanguage(this.props.params.id);
const isDefaultLang = isDefaultLanguage(langCode);
const isValidLang = isValidLanguage(langCode);
if (langCode && !isValidLang) {
this.props.router.push(`/${langCode}/error`);
}
if (langCode && isValidLang) {
const path = getLocalisedPath(langCode, isDefaultLang)
this.props.router.push("/" + path);
}
return (
<div>
<Helmet>
<script type="application/ld+json">{structuredData()}</script>
</Helmet>
<TokenManager>
{(state, methods) => (
<div>
<WithHelmet {...this.props} />
<WithUserHeaderInfo {...this.props} />
<WithStoreDetail />
<WithDataLayerStoreDetail />
<header className='header__nav'>
<NavbarManagerWillHideOnEditor
sideBarOpen={this.state.sideBarOpen}
toggleSidebar={this.toggleSidebar}
languages={this.state.languages}
{...this.props}
/>
<div
className="mmm__overlay"
onClick={this.overlayClickHandler}
style={overlay}
/>
<div
className="mmm__overlay--hidenav"
onClick={this.overlayClickHandler}
style={overlay}
/>
</header>
<main>{this.props.children}</main>
<Modal modalId="modal-account" size="md">
{(closeModal) => (
<Auth
closeModal={closeModal} />
)}
</Modal>
{!this.props.location.pathname.startsWith('/checkout') && <FooterWillHideOnEditor languages={this.state.languages}/>}
</div>
)
}
</TokenManager>
</div>
);
}
}
const mapDispatchToProps = (dispatch, value) => {
const langCode = Cookies.get('langCode') || value.params.id;
const defaultLang = getDefaultLanguage();
const isDefault = isDefaultLanguage(langCode);
const isValid = isValidLanguage(langCode);
const lang = !isValid || isDefault ? defaultLang : langCode;
return {
langCode: () => dispatch({ type: 'SET_LANGUAGE', payload: lang }),
defaultLangCode: () => dispatch({ type: 'SET_DEFAULT_LANGUAGE', payload: defaultLang })
}
}
export default connect(null, mapDispatchToProps)(MainLayout);
let overlay = { display: this.state.sideBarOpen ? 'block' : 'none' };
const langCode = this.handleLanguage(this.props.params.id);
const isDefaultLang = isDefaultLanguage(langCode);
const isValidLang = isValidLanguage(langCode);
if (langCode && !isValidLang) {
this.props.router.push(`/${langCode}/error`);
}
if (langCode && isValidLang) {
const path = getLocalisedPath(langCode, isDefaultLang)
this.props.router.push("/" + path);
}
Here the code is resetting the state/ or mapDispatchToProps dispatched again .that's why it's rerendering.
I found the reason why component keep re-rendering, actually it was because of the condition in:
if (langCode && isValidLang) {
const path = getLocalisedPath(langCode, isDefaultLang)
this.props.router.push("/" + path);
}
which is always true and gets the path and push to the route which cause the component re-render.
Thanks

How do I manage my array of children components' states?

I'm new to react, so forgive me. I'm having a problem understanding states, specifically those of children.
Purpose: I'm trying to create a form that a user can append more and more components -- in this case, images.
What happens: User appends 2 or more images. User tries to upload an image with UploadButton component, but both the images are the same. I believe this has to do with both appended children sharing the same state.
Question: How do I give each appended child its own image without affecting the other appended children?
class Page extends Component
constructor (props) {
super(props);
this.state = {
id: '',
numChildren: 0,
images: [],
}
this.onAddChild = this.onAddChild.bind(this);
}
showModal() {
this.setState({
numChildren: 0,
images: [],
});
}
renderModal()
const children = [];
//Here's my array of child components
for(var i = 0; i < this.state.numChildren; i += 1) {
children.push(<this.ChildComponent key={i} />);
}
return (
<ReactModal>
<this.ParentComponent addChild={this.onAddChild}>
{children}
</this.ParentComponent>
</ReactModal>
)
}
onAddChild = () => {
this.setState({
numChildren: this.state.numChildren + 1
})
}
ParentComponent = (props) => (
<div>
{props.children}
<Button onClick={props.addChild}>Add Item</Button>
</div>
);
ChildComponent = () => (
<div>
<UploadButton
storage="menus"
value={this.state.images}
onUploadComplete={uri => this.setState({images: uri})}
/>
</div>
);
}
Here's the code for UploadButton:
import React, { Component } from 'react';
import uuid from 'uuid';
import firebase from '../config/firebase';
class UploadButton extends Component {
constructor(props) {
super(props);
this.state = {
isUploading: false
}
}
handleClick() {
const input = document.createElement("INPUT");
input.setAttribute("type", "file");
input.setAttribute("accept", "image/gif, image/jpeg, image/png");
input.addEventListener("change", ({target: {files: [file]}}) => this.uploadFile(file));
input.click();
}
uploadFile(file) {
console.log('F', file);
const id = uuid.v4();
this.setState({ isUploading: true })
const metadata = {
contentType: file.type
};
firebase.storage()
.ref('friends')
.child(id)
.put(file, metadata)
.then(({ downloadURL }) => {
this.setState({ isUploading: false })
console.log('Uploaded', downloadURL);
this.props.onUploadComplete(downloadURL);
})
.catch(e => this.setState({ isUploading: false }));
}
render() {
const {
props: {
value,
style = {},
className = "image-upload-button",
},
state: {
isUploading
}
} = this;
return (
<div
onClick={() => this.handleClick()}
className={className}
style={{
...style,
backgroundImage: `url("${this.props.value}")`,
}}>
{isUploading ? "UPLOADING..." : !value ? 'No image' : ''}
</div>
);
}
}
export default UploadButton;
I tried to exclude all unnecessary code not pertaining to my problem, but please, let me know if I need to show more.
EDIT: This is my attempt, it doesn't work:
//altered my children array to include a new prop
renderModal() {
const children = [];
for (var i = 0; i < this.state.numChildren; i += 1) {
children.push(<this.ChildComponent imageSelect={this.onImageSelect} key={i} />);
}
//...
};
//my attempt to assign value and pass selected image back to images array
ChildComponent = () => (
<div>
<UploadButton
storage="menus"
value={uri => this.props.onImageSelect(uri)} //my greenness is really apparent here
onUploadComplete={uri => this.setState({images: uri})}
/>
//...
</div>
);
//added this function to the class
onImageSelect(uri) {
var el = this.state.images.concat(uri);
this.setState({
images: el
})
}
I know I'm not accessing the child prop correctly. This is the most complexity I've dealt with so far. Thanks for your time.
When you write this.state in Child / Parent component, you are actually accessing the state of Page. Now, I would recommend that you pass in the index of the child to the Child like so
children.push(<this.ChildComponent key={i} index={i}/>)
so that each children deals with only its own image like so
ChildComponent = ({index}) => (
<div>
<UploadButton
storage="menus"
value={this.state.images[index]}
onUploadComplete={uri => {
let images = this.state.images.slice()
images[index] = uri
this.setState({images})
}}
/>
</div>
);

React function - is not defined no-undef

I get the following error when trying to compile my app 'handleProgress' is not defined no-undef.
I'm having trouble tracking down why handleProgress is not defined.
Here is the main react component
class App extends Component {
constructor(props) {
super(props);
this.state = {
progressValue: 0,
};
this.handleProgress = this.handleProgress.bind(this);
}
render() {
const { questions } = this.props;
const { progressValue } = this.state;
const groupByList = groupBy(questions.questions, 'type');
const objectToArray = Object.entries(groupByList);
handleProgress = () => {
console.log('hello');
};
return (
<>
<Progress value={progressValue} />
<div>
<ul>
{questionListItem && questionListItem.length > 0 ?
(
<Wizard
onChange={this.handleProgress}
initialValues={{ employed: true }}
onSubmit={() => {
window.alert('Hello');
}}
>
{questionListItem}
</Wizard>
) : null
}
</ul>
</div>
</>
);
}
}
Your render method is wrong it should not contain the handlePress inside:
You are calling handlePress on this so you should keep it in the class.
class App extends Component {
constructor(props) {
super(props);
this.state = {
progressValue: 0,
};
this.handleProgress = this.handleProgress.bind(this);
}
handleProgress = () => {
console.log('hello');
};
render() {
const { questions } = this.props;
const { progressValue } = this.state;
const groupByList = groupBy(questions.questions, 'type');
const objectToArray = Object.entries(groupByList);
return (
<>
<Progress value={progressValue} />
<div>
<ul>
{questionListItem && questionListItem.length > 0 ?
(
<Wizard
onChange={this.handleProgress}
initialValues={{ employed: true }}
onSubmit={() => {
window.alert('Hello');
}}
>
{questionListItem}
</Wizard>
) : null
}
</ul>
</div>
</>
);
}
}
If you are using handleProgress inside render you have to define it follows.
const handleProgress = () => {
console.log('hello');
};
if it is outside render and inside component then use as follows:
handleProgress = () => {
console.log('hello');
};
If you are using arrow function no need to bind the function in constructor it will automatically bind this scope.
handleProgress should not be in the render function, Please keep functions in you component itself, also if you are using ES6 arrow function syntax, you no need to bind it on your constructor.
Please refer the below code block.
class App extends Component {
constructor(props) {
super(props);
this.state = {
progressValue: 0,
};
// no need to use bind in the constructor while using ES6 arrow function.
// this.handleProgress = this.handleProgress.bind(this);
}
// move ES6 arrow function here.
handleProgress = () => {
console.log('hello');
};
render() {
const { questions } = this.props;
const { progressValue } = this.state;
const groupByList = groupBy(questions.questions, 'type');
const objectToArray = Object.entries(groupByList);
return (
<>
<Progress value={progressValue} />
<div>
<ul>
{questionListItem && questionListItem.length > 0 ?
(
<Wizard
onChange={this.handleProgress}
initialValues={{ employed: true }}
onSubmit={() => {
window.alert('Hello');
}}
>
{questionListItem}
</Wizard>
) : null
}
</ul>
</div>
</>
);
}
}
Try this one, I have check it on react version 16.8.6
We don't need to bind in new version using arrow head functions. Here is the full implementation of binding argument method and non argument method.
import React, { Component } from "react";
class Counter extends Component {
state = {
count: 0
};
constructor() {
super();
}
render() {
return (
<div>
<button onClick={this.updateCounter}>NoArgCounter</button>
<button onClick={() => this.updateCounterByArg(this.state.count)}>ArgCounter</button>
<span>{this.state.count}</span>
</div>
);
}
updateCounter = () => {
let { count } = this.state;
this.setState({ count: ++count });
};
updateCounterByArg = counter => {
this.setState({ count: ++counter });
};
}
export default Counter;

adding hammerjs to a react js component properly

I try to add hammer js to my reactjs component and my component looks as it follows
import React from 'react';
import _ from 'underscore';
import Hammer from 'hammerjs';
class Slider extends React.Component {
constructor(props) {
super(props)
this.updatePosition = this.updatePosition.bind(this);
this.next = this.next.bind(this);
this.prev = this.prev.bind(this);
this.state = {
images: [],
slidesLength: null,
currentPosition: 0,
slideTransform: 0,
interval: null
};
}
next() {
console.log('swipe')
const currentPosition = this.updatePosition(this.state.currentPosition - 10);
this.setState({ currentPosition });
}
prev() {
if( this.state.currentPosition !== 0) {
const currentPosition = this.updatePosition(this.state.currentPosition + 10);
this.setState({currentPosition});
}
}
componentDidMount() {
this.hammer = Hammer(this._slider)
this.hammer.on('swipeleft', this.next());
this.hammer.on('swiperight', this.prev());
}
componentWillUnmount() {
this.hammer.off('swipeleft', this.next())
this.hammer.off('swiperight', this.prev())
}
handleSwipe(){
console.log('swipe')
}
scrollToSlide() {
}
updatePosition(nextPosition) {
const { visibleItems, currentPosition } = this.state;
return nextPosition;
}
render() {
let {slides, columns} = this.props
let {currentPosition} = this.state
let sliderNavigation = null
let slider = _.map(slides, function (slide) {
let Background = slide.featured_image_url.full;
if(slide.status === 'publish')
return <div className="slide" id={slide.id} key={slide.id}><div className="Img" style={{ backgroundImage: `url(${Background})` }} data-src={slide.featured_image_url.full}></div></div>
});
if(slides.length > 1 ) {
sliderNavigation = <ul className="slider__navigation">
<li data-slide="prev" className="" onClick={this.prev}>previous</li>
<li data-slide="next" className="" onClick={this.next}>next</li>
</ul>
}
return <div ref={
(el) => this._slider = el
} className="slider-attached"
data-navigation="true"
data-columns={columns}
data-dimensions="auto"
data-slides={slides.length}>
<div className="slides" style={{ transform: `translate(${currentPosition}%, 0px)`, left : 0 }}> {slider} </div>
{sliderNavigation}
</div>
}
}
export default Slider;
the problem is like on tap none of the components method are fired.
How do I deal in this case with the hammer js events in componentDidMount
Reason is, inside componentDidMount lifecycle method swipeleft and swiperight expect the functions but you are assigning value by calling those methods by using () with function name. Remove () it should work.
Write it like this:
componentDidMount() {
this.hammer = Hammer(this._slider)
this.hammer.on('swipeleft', this.next); // remove ()
this.hammer.on('swiperight', this.prev); // remove ()
}

Resources