Cannot update a component (`Back`) while rendering a different component - react-native

I know this question has been answered many times, but I have looked through all the answers and none of them look like my case. I have a JSX component which is basically a header, and in it I have placed another JSX component which is a backpress button. In that backpress button, I have an SVG which is wrapped inside of a Pressable. Now I want the color of the SVG to change when pressed. To achieve that, I have the following code:
function Back({style, onPress}) {
const [pressed, setPressed] = useState(false);
const [color, setColor] = useState('white');
// const Change = useCallback(async (colour) => {
// setColor(colour);
// }, [color]);
useEffect(async () => {
if (pressed) {
await setColor('#c5e2e8');
} else {
await setColor('white');
}
}, [color, pressed]);
return (
<Pressable
style={({pressed}) => {
pressed ? setPressed(true) : setPressed(false);
[style];
}}
onPress={onPress}>
<Svg
xmlns="http://www.w3.org/2000/svg"
version="1.1"
height={36}
width={36}
viewBox="0 0 512 512"
fill={color}>
<Path d="somerandompath" />
</Svg>
</Pressable>
);
}
The problem is that when I press on the back button, I get the warning Cannot update a component (Back) while rendering a different component
As you can see, I tried to put it inside an useEffect. However, it did not help solve my problem.

Okay I have tried to solve this for many hours, and 5 mins after posting here I managed to solve it! Here is the answer:
Make use of the OnPressIn and OnPressOut events of the Pressable component. I removed the entire styles. Here is the new code:
function Back({style, onPress}) {
const [color, setColor] = useState('white');
const onPressIn = async () => {
await setColor('#c5e2e8');
}
const onPressOut = async () => {
await setColor('white');
}
return (
<Pressable
onPressIn={onPressIn}
onPressOut={onPressOut}
onPress={onPress}>
<Svg
xmlns="http://www.w3.org/2000/svg"
version="1.1"
height={36}
width={36}
viewBox="0 0 512 512"
fill={color}>
<Path d="somepathcode" />
</Svg>
</Pressable>
);
}

Related

React Native: Extra empty space on top of the screen

I have a bug where a user clicks on a survey and then opens up what is called supporting information that expands the UI further, then the user selects his or her answer and clicks on the NEXT QUESTION button, at that point the whole top part of the screen drops down exposing this huge gap. This is the code I believe governs all that behavior:
class BallotSurveyDetails extends PureComponent {
componentDidUpdate(prevProps) {
if (prevProps.currentWizardPage !== this.props.currentWizardPage) {
this.scroll.props.scrollToPosition(0, 0, true);
}
}
render() {
const {
currentWizardPage,
selectedSurvey,
handleNextQuestionButtonPress,
handleResponseChanged,
loading,
responses,
handleSubmitButtonPress,
saving,
wizardPages
} = this.props;
if (!saving && loading) {
return <Loading />;
}
const isWizard = selectedSurvey.Layout !== "Wizard";
const isList = selectedSurvey.Layout !== "List";
const displayNextQ = isWizard && currentWizardPage < wizardPages;
const displaySubmit =
isList || (isWizard && currentWizardPage === wizardPages);
const sortedGroups = (selectedSurvey.QuestionGroups || []).sort(
(a, b) => a.Order - b.Order
);
const wizardGroup = isWizard ? sortedGroups[currentWizardPage - 1] : null;
return (
<SafeAreaView style={styles.container}>
{isWizard && wizardPages.length > 1 && (
<Card style={styles.pagination}>
<RadioPagination
numberOfPages={wizardPages}
currentPage={currentWizardPage}
/>
</Card>
)}
<KeyboardAwareScrollView
showsVerticalScrollIndicator={false}
extraScrollHeight={45}
innerRef={ref => {
this.scroll = ref;
}}
enableOnAndroid={true}
contentContainerStyle={{ paddingBottom: 90 }}
>
<View style={styles.headerContainer}>
<Text style={styles.ballotTitle}>{selectedSurvey.Name}</Text>
<Text style={styles.ballotSubtitle}>
{selectedSurvey.Description}
</Text>
</View>
{isList &&
What I tried to do to resolve this was add automaticallyAdjustContentInsets={false} inside the KeyboardAwareScrollView, did nothing to resolve the bug. Any ideas anyone?
I'm not sure what's causing this for you, but here are a few things that have corrected similar problems I've had in the past:
It can help to wrap every screen in a container with flex:1.
I had a similar case with conditionally rendering a search bar above a FlatList and I used this to fix it:
I added this to the top of my file.
import { Dimensions, other stuff you need} from 'react-native';
const deviceHieght = Dimensions.get('window').height;
and then I wrapped my FlatList in a view like this
<View style={this.state.showBar === false ? styles.containFlatlist : styles.containSearchFlatlist}>
and this is the styling it was referencing
containFlatlist: {
height: deviceHieght
},
containSearchFlatlist: {
height: deviceHieght-100
},
In a different similar case I had an issue like this with a screen that displayed photos on click within a scrollview. In that case I did this:
<ScrollView
ref={component => this._scrollInput = component}
>
Then in componentDidMount I put
setTimeout(() => {
this._scrollInput.scrollTo({ x: 0, animated: false })
}, 100)
I was also using react navigation in this case so I also did
return(<View style={styles.mainFlex}>
<NavigationEvents
onWillBlur={payload => this._scrollInput.scrollTo({x:0})}
/>
Followed by the rest of my code.
I hope one of those helps. Given that you're also dealing with a scrollview, my best guess is that the third fix is most likely to work in your situation.
So the appear is with this code snippet here:
componentDidUpdate(prevProps) {
if (prevProps.currentWizardPage !== this.props.currentWizardPage) {
this.scroll.props.scrollToPosition(0, 0, true);
}
}
In particular, this.scroll.props.scrollToPosition(0, 0, true);. In removing the whole component lifecycle method, the bug went away.

How to solve blink image in react-native-snap-carousel?

How to solve blink image when back to first item in react-native-snap-carousel ? I try to look for many examples but fail all.
This is my script :
renderSlider ({item, index}) {
return (
<View style={styles.slide}>
<Image source={{uri: item.cover}} style={styles.imageSlider} />
</View>
);
}
<Carousel
ref={(c) => { this._slider1Ref = c; }}
data={data}
renderItem={this.renderSlider}
sliderWidth={width}
itemWidth={(width - 30)}
itemWidth={(width - 30)}
inactiveSlideScale={0.96}
inactiveSlideOpacity={1}
firstItem={0}
enableMomentum={false}
lockScrollWhileSnapping={false}
loop={true}
loopClonesPerSide={100}
autoplay={true}
activeSlideOffset={50}
/>
the comple documentation you can find here and about the plugin api you can find here.
Please anyone help me.
Thanks.
I had the same issue when loop={true} was set.
We came up with this workaround:
We maintained the activeSlide value in a state, and created a reference of Carousel refCarousel.
const [activeSlide, setActiveSlide] = useState(0);
const refCarousel = useRef();
Then we added code in useEffect to manually move the carousel item to the first one back when it reaches the end with a delay of 3500 milliseconds which is also set to autoplayInterval props.
This way, we achieved the looping effect.
useEffect(() => {
if (activeSlide === data.length - 1) {
setTimeout(() => {
refCarousel.current.snapToItem(0);
}, 3500)
}
}, [activeSlide]);
Below is the Carousel component declaration. Only the relevant props are shown here.
<Carousel
ref={refCarousel}
...
//loop={true}
autoplay={true}
autoplayDelay={500}
autoplayInterval={3500}
onSnapToItem={(index) => setActiveSlide(index)}
/>
use React Native Fast Image if you are facing blinking issue.

Components not reacting to nested mobx stores

I'm rendering a square based off a nested store (SquareModel) in MobX. When the square is clicked I would like an action to fire within SquareModel, changing a selected property which would, in turn, resize the square. However, I can't seem to get the square to react to the property change. Any ideas on how to get this working?
Sandbox: https://codesandbox.io/s/m51oyzz069
MainStore:
import { observable, decorate } from "mobx";
import SquareModel from "./SquareModel";
class MobXStore {
square = new SquareModel();
}
decorate(MobXStore, {
square: observable
});
export default new MobXStore();
NestedStore:
import { observable, action, decorate } from "mobx";
class SquareModel {
selected = false;
toggleSelection() {
console.log("toggle selection");
this.selected = !this.selected;
}
}
decorate(SquareModel, {
selected: observable,
toggleSelection: action
});
export default SquareModel;
React:
const App = observer(() => {
return (
<svg className="App">
<svg x={100} y={100}>
<Square
unit={MobXStore.square.selected ? 2 : 1}
onPointerUp={MobXStore.square.toggleSelection}
/>
</svg>
</svg>
);
});
export const Square = observer(({ unit, ...props }) => {
console.log("render square");
return (
<g {...props}>
<rect height={unit * 50} width={unit * 50} style={{ fill: "blue" }} />
</g>
);
});
There is an error with "this" in the toggleSelection method
It should look like:
onPointerUp = {() => MobXStore.square.toggleSelection()}
or you need to use, for example, the arrow function here:
toggleSelection = () => {

How to correctly large state updates in React Native?

I am writing a small ReactNative application that allows users to invite people to events.
The design includes a list of invitees, each of which is accompanied by a checkbox used to invite/uninvite said invitee. Another checkbox at the top of the list that performs a mass invite/uninvite on all invitees simultaneously. Finally a button will eventually be used to send out the invites.
Because the state of each of these elements depends changes made by the other I often need to re-render my entire UI whenever the user takes action on one of them. But while this works correctly it is causing me quite a few performance issues, as shown in this video
Here's the code I'm using:
import React, { Component } from 'react';
import { Container, Header, Title,
Content, Footer, FooterTab,
Button, Left, Right,
Center, Body, Text, Spinner, Toast, Root , CheckBox, ListItem, Thumbnail} from 'native-base';
import { FlatList, View } from 'react-native';
export default class EventInviteComponent extends Component {
constructor(props) {
super(props);
console.disableYellowBox = true;
this.state = {
eventName: "Cool Outing!",
invitees:[]
}
for(i = 0; i < 50; i++){
this.state.invitees[i] = {
name: "Peter the " + i + "th",
isSelected: false,
thumbnailUrl: 'https://is1-ssl.mzstatic.com/image/thumb/Purple111/v4/62/08/7e/62087ed8-5016-3ed0-ca33-50d33a5d8497/source/512x512bb.jpg'
}
}
this.toggelSelectAll = this.toggelSelectAll.bind(this)
}
toggelSelectAll(){
let invitees = [...this.state.invitees].slice();
let shouldInviteAll = invitees.filter(invitee => !invitee.isSelected).length != 0
let newState = this.state;
newState = invitees.map(function(invitee){
invitee.isSelected = shouldInviteAll;
return invitee;
});
this.setState(newState);
}
render() {
let invitees = [...this.state.invitees];
return (
<Root>
<Container>
<Content>
<Text>{this.state.eventName}</Text>
<View style={{flexDirection: 'row', height: 50, marginLeft:10, marginTop:20}}>
<CheckBox
checked={this.state.invitees.filter(invitee => !invitee.isSelected).length == 0}
onPress={this.toggelSelectAll}/>
<Text style={{marginLeft:30 }}>Select/deselect all</Text>
</View>
<FlatList
keyExtractor={(invitee, index) => invitee.name}
data={invitees}
renderItem={(item)=>
<ListItem avatar style={{paddingTop: 20}}>
<Left>
<Thumbnail source={{ uri: item.item.thumbnailUrl}} />
</Left>
<Body>
<Text>{item.item.name}</Text>
<Text note> </Text>
</Body>
<Right>
<CheckBox
checked={item.item.isSelected}/>
</Right>
</ListItem>}/>
</Content>
<Footer>
<FooterTab>
<Button full
active={invitees.filter(invitee => invitee.isSelected).length > 0}>
<Text>Invite!</Text>
</Button>
</FooterTab>
</Footer>
</Container>
</Root>);
}
}
In your code, in class method toggelSelectAll() {...} you modify the state directly by using this.state = ..., which is something to be avoided. Only use this.state = ... in your class constructor() {...} to initialize the state, and you should only use this.setState({...}) to update the state anywhere else.
Not sure if this should help your performance issues, but try replacing toggelSelectAll() with the following:
toggelSelectAll() {
const {invitees} = this.state;
const areAllSelectedAlready = invitees.filter(({isSelected}) => !isSelected).length === 0;
this.setState({
invitees: invitees.map(invitee => ({
...invitee,
isSelected: !areAllSelectedAlready
}))
});
}
Good luck! And, let me know if you would like me to refactor your above code to remove the 2nd this.state = ... in your constructor (which, once again, should be avoided when writing React).
I suggest:
Dividing your code by creating multiple components, so you won't have a massive render()
Using Redux to store invitee / global state, so you can choose which components should re-render in case of modifications
That's a good way to learn React Native!

Reacting to a component being scrolled off screen in a react-native ScrollView

Currently I am using a <ScrollView /> component to handle the scrolling for my items, as I will only ever have a maximum of three items I feel this is appropriate instead of introducing a <FlatList />. This component receives a prop called collapsed and onCollapseToggle which is used to modify the collapse prop that is passed to the child. I have also experimented with the child having it's collapsed variable in state, but it seems that modifying the child's state from the parent would be near-impossible.
When scrolling through the <ScrollView /> when a component is passed up (The user scrolls down far enough that the component is no longer displayed on the screen) I want to execute a function that could potentially change the collapsed value that's passed to the item being rendered. This way if a user as expanded an item to view more information about it, and then continues scrolling down the <ScrollView /> the item would be self-collapsing, without the user having the manually close it through some form of input.
I'm not currently sure about any way to go about this, and any help would be greatly appreciated. I will provide an example of the structure that I am working with, which may help someone come up with a solution. I do not mind restructuring my components.
class ContentInformation extends React.Component {
state = { content: [ ... ] };
onCollapseToggle = (index, displayed=true) => {
const { content } = this.state;
const arr = content.slice();
const item = arr[index];
if(!item) return;
if(!displayed) {
if(!item.collapsed) {
item.collapsed = true;
}
} else {
item.collapsed = !item.collapsed;
}
arr[index] = item;
this.setState({ content: arr });
}
render() {
return (
<ScrollView>
{ this.state.content.map((content, index) => (
<Item
key={content._id}
index={index}
onCollapseToggle={this.onCollapseToggle}
{...content} />
); }
</ScrollView>
);
}
}
So basically, as you can see, I only need to figure out when the <Item /> goes off-screen so I can call onCollapseToggle(index, false) to automatically re-collapse the component if it's open.
This is an example of how you can detect when an item is offscreen when scrolling.
state = { toggleDistance: 0 }
_handleScroll({nativeEvent: {contentOffset: {y}}}) {
const {toggleDistance} = this.state;
if (y >= toggleDistance) {
// The item is offscreen
}
}
render() {
<ScrollView
onScroll={this._handleScroll.bind(this)}>
<Item
onLayout={({nativeEvent: {layout: {y, height}}}) => this.setState({toggleDistance: (y + height)})}/>
...
</ScrollView>
}
Seems to me you need a FlatList or a ListView and ScrollView simply doesn't support this use case.
The hint is in the ScrollView description:
FlatList is also handy if you want ... any number of other features it supports out of the box.
I solve the problem improving the answer from ivillamil with some adjusts:
const [toggleDistance, setToggleDistance] = useState(0);
const [showBottom, setShowBottom] = useState(false);
const onScrollMoveFooter = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
const currentOffset = event.nativeEvent.contentOffset.y;
if (event.nativeEvent.layoutMeasurement.height - currentOffset <
toggleDistance) {
setShowBottom(true);
} else {
setShowBottom(false);
}
};
And in the another component:
<Component
onLayout={({
nativeEvent: {
layout: { y, height },
},
}) => setToggleDistance(y + height)}
/>
and so I can conditionally hide what I wanted:
{showBottom && (<AnotherComponent/>)}