In render() of my RN component class I call functional component A which loads a Carousel. This Carousel in A then calls functional component B in its renderItem prop. B contains a FlatList w ref defined as <FlatList ref={ref => (this.list = ref)} />.
So A is successfully accessing this.list.scrollToIndex() in an onTouch call defined in A. What I need to do is call this.list.scrollToOffset() in B where the FlatList and the ref are defined - but when I do that I get an error TypeError: TypeError: null is not an object (evaluating '_this.list.scrollToOffset')
I've tried using React.createRef() at the class level but that is not working either.
Here is the code:
//component B
renderList = () => {
if (scrollPosition>0) {
//this call to list fails
this.list.scrollToOffset({
offset: scrollPosition
})
}
return (
<FlatList
ref={ref => (this.list = ref)}
...
/>
)
}
//component A
renderCarousel = () => {
return (
<View style={{ flex: 1 }}>
<View>
<TouchableOpacity
onPress={() => {
this.list.scrollToIndex({ index: 0 }) //this call to list works
}}
>
</TouchableOpacity>
</View>
{
showList && (
<Carousel
renderItem={this.renderList}
/>
)
}
</View>
)
}
I'm guessing I'm in that weird and dreaded js this zone. Any ideas?? TIA!
Related
Problem:
When using a ScrollView in ReactNative for horizontal pagination it re-renders all children, but I would like to keep the state values of certain local input fields and local variables of children components.
In the code below, if I were in the middle of updating a TextInput within the NotesSection but wanted to swipe back to the BatchSection to review some metadata, the code re-renders NotesSection and resets a local state holding the text value.
Diagnosis:
I'm very new to React and React Native, but my best guess here is that this happens due to the parent state variable "horizontalPos" which takes an integer to reflect what page is in focus.
This is simply used in the ProductHeader component to highlight a coloured bottomBorder showing the user a kind of small "menu" at the top of the screen.
The "horizontalPos" state can be updated in 2 ways:
First one is simply when clicking the wanted header (TouchableOpacity) within ProductHeader which triggers a state change and uses useRef to automatically move the ScrollView.
Second option is when the user swipes on the ScrollView. Using OnScroll to run a function "handleHorizontalScroll" which in turn sets the "horizontalPos" state using simple maths from the contentOffset.x.
Question / Solution:
If "horizontalPos" state was INSIDE ProductHeader I suspect this would solve the issue but I can't wrap my mind around how to do this as I don't believe it's possible to pass a function through to the child based on a change in the parent component.
I'm dependent on registering the OnScroll on the main ScrollView and the remaining components likewise have to be inside the main ScrollView but I don't want them to re-render every time the "horizontalPos" state updates.
Code:
const ProductScreen = (props) => {
const [horizontalPos, setHorizontalPos] = useState(0)
const scrollRef = useRef()
const toggleHorizontal = (page) => {
setHorizontalPos(page)
scrollRef.current.scrollTo({x:page*width, y:0, animated:false})
}
const handleHorizontalScroll = (v) => {
const pagination = Math.round(v.nativeEvent.contentOffset.x / width)
if (pagination != horizontalPos){
setHorizontalPos(pagination)
}
}
const ProductHeader = () => {
return(
<View style={styles.scrollHeaderContainer}>
<TouchableOpacity style={[styles.scrollHeader, horizontalPos == 0 ? {borderColor: AppGreenDark,} : null]} onPress={() => toggleHorizontal(0)}>
<Text style={styles.scrollHeaderText}>Meta Data</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.scrollHeader, horizontalPos == 1 ? {borderColor: AppGreenDark,} : null]} onPress={() => toggleHorizontal(1)}>
<Text style={styles.scrollHeaderText}>{"Notes"}</Text>
</TouchableOpacity>
</View>
)
}
return (
<View style={styles.container}>
<ProductHeader/>
<ScrollView
ref={scrollRef}
decelerationRate={'fast'}
horizontal={true}
showsHorizontalScrollIndicator={false}
snapToInterval={width}
onScroll={handleHorizontalScroll}
scrollEventThrottle={16}
disableIntervalMomentum={true}
style={{flex: 1}}
>
<View style={[styles.horizontalScroll]}>
<View style={styles.mainScrollView}>
<BatchSection/>
</View>
<ScrollView style={styles.notesScrollView}>
<NotesSection/>
</ScrollView>
</View>
</ScrollView>
</View>
)
}
As you outlined, updating horizontalPos state inside ProductScreen will cause a whole screen to re-render which is not an expected behavior.
To avoid this scenario, let's refactor the code as below:
function debounce(func, timeout = 500){
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, timeout);
};
}
class ProductHeader extends React.Component {
state = {horizontalPos:0 }
toggleHorizontal = (page) => {
this.setState({horizontalPos:page});
this.props.onPositionChange(page);
};
render () {
const {horizontalPos} = this.state
return (
<View style={styles.scrollHeaderContainer}>
<TouchableOpacity
style={[
styles.scrollHeader,
horizontalPos == 0 ? { borderColor: AppGreenDark } : null,
]}
onPress={() => this.toggleHorizontal(0)}
>
<Text style={styles.scrollHeaderText}>Meta Data</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.scrollHeader,
horizontalPos == 1 ? { borderColor: AppGreenDark } : null,
]}
onPress={() => this.toggleHorizontal(1)}
>
<Text style={styles.scrollHeaderText}>{"Notes"}</Text>
</TouchableOpacity>
</View>
);
}
};
const ProductScreen = (props) => {
const scrollRef = useRef();
const productHeaderRef = useRef()
let horizontalPos = 0;
const handleHorizontalScroll = (v) => {
const pagination = Math.round(v.nativeEvent.contentOffset.x / width);
if (pagination != horizontalPos) {
productHeaderRef.current?.toggleHorizontal(pagination)
}
};
const debouncedHorizontalScroll= debounce(handleHorizontalScroll,500)
const onPositionChange = (page) => {
horizontalPos = page;
scrollRef.current.scrollTo({ x: page * width, y: 0, animated: false });
};
return (
<View style={styles.container}>
<ProductHeader onPositionChange={onPositionChange} ref={productHeaderRef} />
<ScrollView
ref={scrollRef}
decelerationRate={"fast"}
horizontal={true}
showsHorizontalScrollIndicator={false}
snapToInterval={width}
onScroll={debouncedHorizontalScroll}
scrollEventThrottle={16}
disableIntervalMomentum={true}
style={{ flex: 1 }}
>
<View style={[styles.horizontalScroll]}>
<View style={styles.mainScrollView}>
<BatchSection />
</View>
<ScrollView style={styles.notesScrollView}>
<NotesSection />
</ScrollView>
</View>
</ScrollView>
</View>
);
};
I hope this will stop the whole screen from rerendering and maintaining pagination.
I am trying to print the ComponentTwo Flatlist only once but instead, I am getting the result image1 but instead, I need it to appear like this image 2. I have attached a snack link with the code in it.
Code That will produce the same results as in the images
Expo Snack Link
Working Example: Expo Snack
Here is how you can fix this, first pass the index value to ComponentOne from App.js
const App = () => {
return (
<SafeAreaView style={styles.container}>
<FlatList
data={DATA}
renderItem={({item, index}) => <ComponentOne name={item.title} index={index}/>}
keyExtractor={(item) => item.id}
/>
</SafeAreaView>
);
};
Now use that prop value to render ComponentTwo conditionally in ComponentOne like shown below:
//...const
ComponentOne = (props: ComponentOneProps) => {
return (
<View style={{ margin: 15, backgroundColor: 'yellow' }}>
<FlatList
data={recent}
renderItem={({ item }) => {
console.log("hello")
// #ts-ignore
return props.index == 0?<ComponentTwo name={item.name} />:null;
}}
keyExtractor={(item) => item.idd}
/>
//...
I am trying to set up a React Native ref like here, only in a class component:
https://snack.expo.io/PrrDmZ4pk
Here's my code:
class DetailBody extends Component {
constructor() {
super();
this.myRefs = React.createRef([]);
}
clickText(index) {
this.myRefs.current[index].setNativeProps({ style: { backgroundColor: '#FF0000' } });
}
render() {
if (this.props.article.json.results.length === 0) {
return <Loading />;
}
return (
<View >
<View>
<View ref={this.props.highlight} nativeID="some-id" >
{this.props.article.json.results.map((content, index) => (
<TouchableOpacity onPress={() => this.clickText(index)}>
<View key={index} ref={el => this.myRefs.current[index] = el}>{content}</View>
</TouchableOpacity>
</View>
</View>
</View>
This should theoretically let me add a background colors when my ref is clicked, much like the snack I linked to above.
However what I actually see is this:
This seems to be related to .current inside my ref being null, despite passing a default value.
How do I fix this error?
If the ref callback is defined as an inline function, it will get called twice during updates, first with null and then again with the DOM element.
Haven't really used it this way but I think you might just need to do this in the constructor:
this.myRefs = React.createRef();
this.myRefs.current = [];
I have this code
class Home extends Component {
constructor(props) {
super(props);
this.state = {
dataSource: []
}
this._handleRenderItem = this._handleRenderItem.bind(this);
this._keyExtractor = this._keyExtractor.bind(this);
}
componentDidMount() {
let success = (response) => {
this.setState({ dataSource: response.data });
};
let error = (err) => {
console.log(err.response);
};
listarProdutos(success, error);
}
_keyExtractor = (item, index) => item._id;
_handleRenderItem = (produto) => {
return (
<ItemAtualizado item={produto.item} />
);
}
render() {
return (
<Container style={styles.container}>
<Content>
<Card>
<CardItem style={{ flexDirection: 'column' }}>
<Text style={{ color: '#323232' }}>Produtos atualizados recentemente</Text>
<View style={{ width: '100%' }}>
<FlatList
showsVerticalScrollIndicator={false}
data={this.state.dataSource}
keyExtractor={this._keyExtractor}
renderItem={this._handleRenderItem}
/>
</View>
</CardItem>
</Card>
</Content>
</Container>
);
}
}
export default Home;
The function _handleRenderItem() is being called twice and I can't find the reason. The first time the values inside my <ItemAtualizado /> are empty, but the second was an object.
This is normal RN behavior. At first, when the component is created you have an empty DataSource ([]) so the FlatList is rendered with that.
After that, componentDidMount triggers and loads the updated data, which updates the DataSource.
Then, you update the state with the setState which triggers a re render to update the FlatList.
All normal here. If you want to try, load the datasource in the constructor and remove the loading in the componentDidMount. It should only trigger once.
If you want to control render actions, you can use shouldComponentUpdate method.
For example:
shouldComponentUpdate(nextProps, nextState){
if(this.state.friends.length === nextState.friends.lenght)
return false;
}
it will break render if friends count not change.
I recreated the issue in this snack. https://snack.expo.io/B1KoX-EUN - I confirmed you can use shouldComponentUpdate(nextProps, nextState) to diff this.state or this.props and return true/false - https://reactjs.org/docs/react-component.html#shouldcomponentupdate docs say this callback should be used only for optimization which is what we're doing here.
I have a button that when clicked calls a function that sorts the products by case amount. I am updating the products array so I assumed this would trigger a re-render of the products being mapped in the code below but it is not. Does anyone know how to get this to trigger the products.map to be re-rendered again thus displaying the new sorted products?
render() {
const {products} = this.props;
const cartIcon = (<Icon name="shopping-cart" style={styles.cartIcon} />);
sortCartItems = () => {
products.sort((a, b) => a.cases > b.cases);
}
return (
<View style={styles.itemsInCartContent}>
<View style={styles.cartHeader}>
<TouchableOpacity onPress={sortCartItems}>
{cartIcon}
<Text>Sort</Text>
</TouchableOpacity>
</View>
{products.map((product, index) =>
<CartProductItem
ref="childCartProductItem"
key={product.id}
product={product}
index={index}
activeIndex={this.state.active}
triggerParentUpdate={() => this.collapseAllOtherProducts}
/>
)}
</View>
);
}
A component should not mutate it's own props. If your data changes during the lifetime of a component you need to use state.
Your inline arrow function sortCartItems tries to mutate the products that come from props. Your need to store the products in the components state instead and call setState to change them which will trigger a re-render.
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
products: props.products,
}
}
sortCartItems = () => {
this.setState(prevState => ({products: prevState.products.sort((a, b) => a.cases > b.cases);}))
}
render() {...}
}
Note that you need to use a callback in setState whenever you are updating the state based on the previous state. The callback receives the old state as a parameter and returns the new state.
I used a combination of messerbill's and trixn's answers to come up with the following which is now working. And I added a products property to state which receives its data from props.products
render() {
const cartIcon = (<Icon name="shopping-cart" style={styles.cartIcon} />);
sortCartItems = () => {
this.setState({
products: this.state.products.sort((a, b) => a.cases > b.cases)
});
}
return (
<View style={styles.itemsInCartContent}>
<View style={styles.cartHeader}>
<TouchableOpacity onPress={sortCartItems}>
{cartIcon}
<Text>Sort</Text>
</TouchableOpacity>
</View>
{this.state.products.map((product, index) =>
<CartProductItem
ref="childCartProductItem"
key={product.id}
product={product}
index={index}
activeIndex={this.state.active}
triggerParentUpdate={() => this.collapseAllOtherProducts}
/>
)}
</View>
);
}