Why this ref.measureLayout is giving me error? - reactjs

I am trying to get the measurement of the tabs.
Here is the data for this test app. I am creating ref here
const images = {
man: 'https://www.gogole.com',
....
};
const data = Object.keys(images).map(i => ({
key: i,
title: i,
image: images[i],
ref: React.createRef(),
}));
Now I am trying to get the measurement of each tab like this
const Tab = React.forwardRef(({item}, ref) => {
return (
<View ref={ref} key={item.title}>
<Text>{item.title}</Text>
</View>
);
});
const Tabs = ({data, scrollX}) => {
const containerRef = useRef({});
React.useEffect(() => {
data && data.forEach((item) => {
item.ref.current.measureLayout(
containerRef.current,
(x, y, width, height) => {
console.log(x, y, width, height);
},
);
});
},[]);
return (
<View
style={{
position: 'absolute',
}}>
{data.map((data, index) => (
<Tab key={index} item={data} ref={data.ref} />
))}
</View>
);
};
return (
<View style={styles.container}>
<Tabs data={data} />
</View>
)
As you can see I am trying to console.log the measurement in the Tabs. But the app doesn't return the measurement but always this line
Warning: ref.measureLayout must be called with a node handle or a ref to a native component.
From my research in other GitHub treads , there is something called relativeToNativeNode which is deprecated now. Is my error related to this?
So, to avoid the error and get the measurement. What should I change in the code?

Related

Passing state from function to component

My entire goal was to navigate from a screen while changing states in the screen I am navigating to. I have successfully done that in a minimal working example, however in my overall project, the screen I am navigating to needs to be passed the state through a couple levels.
I have two examples. In the first example(You must run the examples in IOS or android, you can see what I need to achieve, everything works as it should. You can move from screen3 to the home page and the states change along with the slider button moving.
In the second example, you can see right off the bat I have an error due to my attempt at passing states the same way I do in the original example however there is one more level I need to pass through in this example. You can see by removing line 39 in this demo, it removes the error so obviously I am not passing states correctly. I need to pass states from Home to Top3 to Slider
Here is example 1 and here is example 2 while I have also provided some code below that highlights the differences where the error occurs in the two examples.
Any insight at all is appreciated more than you know! Thank you.
Example1 -> you can see I directly render the slider button which causes zero issues.
const Home = ({ route }) => {
const [isVisile, setIsVisible] = React.useState(true);
const [whichComponentToShow, setComponentToShow] = React.useState("Screen1");
React.useEffect(() => {
if(route.params && route.params.componentToShow) {
setComponentToShow(route.params.componentToShow);
}
}, [route.params]);
const goToMap = () => {
setComponentToShow("Screen2");
}
const goToList = () => {
setComponentToShow("Screen1");
}
return(
<View style={{backgroundColor: '#d1cfcf' ,flex: 1}}>
{whichComponentToShow === 'Screen1' && <ListHome />}
{whichComponentToShow === 'Screen2' && <MapHome />}
<View style={{position: 'absolute', top: 0, left: 0, right: 1}}>
<Slider
renderMap={goToMap}
renderList={goToList}
active={route.params && route.params.componentToShow==='Screen2'|| false}
/>
</View>
</View>
);
}`
Example2 -> You can see I render Slider in a file called Top3, I am struggling to pass these states from Home to Top3 to Slider.
const [isVisile, setIsVisible] = React.useState(true);
const [whichComponentToShow, setComponentToShow] = React.useState("Screen1");
React.useEffect(() => {
if(route.params && route.params.componentToShow) {
setComponentToShow(route.params.componentToShow);
goToMap()
}
}, [route.params]);
const goToMap = () => {
setComponentToShow("Screen2");
}
const goToList = () => {
setComponentToShow("Screen1");
}
return(
<View style={{backgroundColor: '#d1cfcf' ,flex: 1}}>
{whichComponentToShow === 'Screen1' && <ListHome />}
{whichComponentToShow === 'Screen2' && <MapHome />}
<View style={{position: 'absolute', top: 0, left: 0, right: 1}}>
<Top3
renderMap={goToMap}
renderList={goToList}
active={route.params && route.params.componentToShow==='Screen2'|| false}
/>
</View>
</View>
);
}
Top3
export default class Top3 extends React.Component {
goToMap = () => {
this.props.renderMap();
};
goToList = () => {
this.props.renderList();
};
render() {
return (
<View>
<Slider renderMap={this.goToMap.bind(this)}
renderList={this.goToList.bind(this)}
active={active}/>
</View>
);
}
}
from your examples, I think you are not extracting active from props properly.
here is the demo working code your example2 code https://snack.expo.dev/4atEkpGVo
here is the sample code for component Top3
export default class Top3 extends React.Component {
goToMap = () => {
this.props.renderMap();
};
goToList = () => {
this.props.renderList();
};
render() {
const {active=false} = this.props;
return (
<View>
<Slider renderMap={this.goToMap.bind(this)}
renderList={this.goToList.bind(this)}
active={active}/>
</View>
);
}
}
if you want to share states between multiple screens, then you might want to use global stores like react context api or redux instead of passing states to each screen that would be simple

Checking for viewable items in a Flatlist and passing a prop if the item being rendered id matches a viewable items id

So basically what I said in the title, I am simply trying to change a prop that im passing to the component of Post if the item currently being rendered is in the viewport.
I am getting double the output like its firing twice, and its not even correct.
im comparing a key to the id (same thing) and if the 'activeVideo !== item.id' (video id's) are the same
the video should play, because i pass 'False' to the 'paused' property.
question is why am i getting double the output and why are both videos being paused when one of them clearly shouldnt>?
Need help fast, its for a school project.
Home.js
const [activeVideo, setActiveVideo] = useState(null);
const onViewRef = React.useRef((viewableItems)=> {
setActiveVideo(viewableItems.changed[0].key)
})
const viewConfigRef = React.useRef({ viewAreaCoveragePercentThreshold: 75 })
return (
<View>
<FlatList
data={posts}
showsVerticalScrollIndicator={false}
snapToInterval={Dimensions.get('window').height - 24}
snapToAlignment={'start'}
decelerationRate={'fast'}
onViewableItemsChanged={onViewRef.current}
viewabilityConfig={viewConfigRef.current}
renderItem={({item}) => <Post
post={item}
paused={activeVideo !== item.id}
/>}
/>
</View>
)}
Post.js
const Post = (props) => {
const [post, setPost] = useState(props.post);
const [paused, setPaused] = useState(props.paused);
console.log(props.paused)
const onPlayPausePress = () => {
setPaused(!paused);
};
const onPlayPausePress2 = () => {
setPaused(!paused);
};
return (
<View style={styles.container}>
<TouchableWithoutFeedback>
<Image
source={{uri: post.poster}}
style={styles.video2}
/>
</TouchableWithoutFeedback>
<TouchableWithoutFeedback onPress={onPlayPausePress}>
<Video
source={post.postUrl}
style={styles.video}
onError={(e)=> console.log(e)}
resizeMode={'cover'}
repeat
paused={paused}
/>
</TouchableWithoutFeedback>
</View>
)}

Rendering specific component in React Native

I used the ScrollView's onMomentumScrollEnd handler to determine the current page based on the contentOffset in the recyclerlistview Component.
const [currentPage, setCurrentPage] = useState(0);
const onScrollEnd = (e) => {
let contentOffset = e.nativeEvent.contentOffset;
let viewSize = e.nativeEvent.layoutMeasurement;
let pageNum = Math.floor(contentOffset.x / viewSize.width);
setCurrentPage(pageNum);
};
return (
<View style={{ flex: 1 }}>
<RecyclerListView
rowRenderer={({ item }, index) => <Page item={item} id={index} />}
dataProvider={list}
layoutProvider={layoutProvider}
onMomentumScrollEnd={onScrollEnd}
isHorizontal
pagingEnabled
/>
<View style={{ flex: 0.1, backgroundColor:"gold" }}>
<Text>{`current page: ${currentPage}`}</Text>
</View>
</View>
);
I want to display the current Page inside a Text component, but when currentPage state changes in onMomentumScrollEnd handler the whole app is re-render. I need to re-render only the Text Component, any suggestion for that.
I find a solution. the easiest way is to create a class component and put the displayed component in it, then I reference it and change the current page with ref hook.
class DisplayCurrentPage extends React.Component {
contractor(prop){
super(prop)
state:{currentPage:0}
}
updateCurrentPage = (index) => {this.setState({CurrentPage: index})}
render(){
return(
<View style={{ flex: 0.1, backgroundColor:"gold" }}>
<Text>{`current page: ${this.state.currentPage}`}</Text>
</View>
);
}
};
const reference = createRef();
const onScrollEnd = (e) => {
let contentOffset = e.nativeEvent.contentOffset;
let viewSize = e.nativeEvent.layoutMeasurement;
let pageNum = Math.floor(contentOffset.x / viewSize.width);
reference.current.updateCurrentPage(pageNum);
};
return (
<View style={{ flex: 1 }}>
<RecyclerListView
rowRenderer={({ item }, index) => <Page item={item} id={index} />}
dataProvider={list}
layoutProvider={layoutProvider}
onMomentumScrollEnd={onScrollEnd}
isHorizontal
pagingEnabled
/>
<DisplayCurrentPage ref={reference} />
</View>
);
When the page is changed, only DisplayCurrentPage is re-render.
You can use React.memo which is an alternative to shouldComponentUpdate for functional components. It tells React when to re-render the component based on prev and next props.
const Item = React.memo(({ item }) => {
return (
<View>
........
</View>
);
});

React Native FlatList onViewableItemsChanged callback encounter error after state changed rerender

I need to set the state to detect which item in the current viewport is visible. for this purpose, I write below code:
const [inViewPort, setInViewPort] = useState(0);
const viewabilityConfig = {
viewAreaCoveragePercentThreshold: 30,
};
const onViewableItemsChanged = ({viewableItems, changed}) => {
if (changed && changed.length > 0) {
setInViewPort(changed[0].index);
}
};
return (
<SafeAreaView style={styles.container}>
<FlatList
data={myData}
renderItem={renderItem}
showsHorizontalScrollIndicator={false}
keyExtractor={(_, index) => index.toString()}
horizontal={true}
onViewableItemsChanged={onViewableItemsChanged}
viewabilityConfig={viewabilityConfig}
/>
</SafeAreaView>
);
The onViewableItemsChanged event callback triggers correctly but after I call setInViewPort so the component updated and it rerendered the below error encounter:
Invariant Violation: Changing onViewableItemsChanged on the fly is not supported
I was looking for the same answer and there is no clear example anywhere. This is how I solved it.
const [inViewPort, setInViewPort] = useState(0)
const viewabilityConfig = useRef({
itemVisiblePercentThreshold: 50,
waitForInteraction: true,
minimumViewTime: 5,
})
const onViewableItemsChanged = React.useRef(({ viewableItems, changed }) => {
if (changed && changed.length > 0) {
setInViewPort(changed[0].index);
}
})
return (
<SafeAreaView style={styles.container}>
<FlatList
data={myData}
renderItem={renderItem}
showsHorizontalScrollIndicator={false}
keyExtractor={(_, index) => index.toString()}
horizontal={true}
onViewableItemsChanged={onViewableItemsChanged.current}
viewabilityConfig={viewabilityConfig.current}
/>
</SafeAreaView>
)

Is setting a value attribute in TextInput necessary?

I was having issues with something like this (specifically in the TextInput value attribute):
const Stuff = props => {
const [items, setItems] = useState([]);
const handleNewItem = () => {
setItems([...items, '']);
};
const handleText = (text, index) => {
items[index] = text;
setItems(items);
// this was populating correctly in console.log
// as I type, it will come out like ["somedata", "blah"....] etc...
};
return (
<>
<View style={{marginTop: 20}}>
<View>
{items.map((items, index) => {
return (
<View key={index}>
<Text>{index + 1}</Text>
// issue with setting value attribute
// Text would disappear as I type in the input field
<TextInput value={items} onChangeText={text => handleText(text, index)} />
</View>
);
})}
<TouchableOpacity onPress={e => handleNewItem(e)}>
<Text>Add item</Text>
</TouchableOpacity>
</View>
</View>
</>
);
};
I was able to get console logged out the correct values for items, but on my mobile simulator, when I type something, the text disappears.
When I removed value={items} from the TextInput component, I'm able to type in the simulator input field, without the text disappearing. I always thought we needed a value from reactjs. Do we not need this? or am I doing something wrong?
I would suggest don't directly update your state. Instead use new object to update the state like
const handleText = (text, index) => {
let newItems = [...items];
newItems[index] = text;
setItems(newItems);
};

Resources