Screen appears to unexpectedly render twice after mounting. React Native - react-native

I'm trying to solve an issue I'm having with RN. I'm working with the Philips Hue API. I have one screen that is part of a tab navigation and I have a stack navigation nested inside one of the tabs. I have a screen that shows all lights connected to the hub and when you select one lightbulb it navigates you to a detail screen.
Below is my "All Lights" screen:
<FlatList
data={allLights}
renderItem={({ item, index }) => {
console.log(item, "from light icon");
return (
<TouchableOpacity
onPress={() =>
navigation.navigate("LightDetail", {
lightbulb: item,
lightId: index + 1,
})
}
>
<LightBulb
lightName={item.name}
isOn={item.state.on}
lightId={index + 1}
/>
</TouchableOpacity>
);
}}
/>
You will notice I'm passing in data as 'lightbulb'.
Below is my "Light Detail" screen:
export const LightDetailScreen = ({ route }) => {
const { lightbulb, lightId } = route.params;
console.log(lightbulb.state.sat, lightbulb.state.bri, "from params");
const [lightBulbState, setLightBulbState] = useState({
sat: lightbulb.state.sat,
hue: lightbulb.state.hue,
bri: lightbulb.state.bri,
isOn: lightbulb.state.on,
});
console.log(lightBulbState);
const controlLight = async (lightID, on, hue, sat, bri) => {
console.log(hue, sat, bri, "from control light");
try {
return await huePhil.put(\/lights/${lightID}/state`, {on,sat,bri,hue, }); } catch (err) {console.log(err); } };const toggleLight = async () => {setLightBulbState((prev) => {return { ...prev, isOn: !prev.isOn }; });controlLight(lightId,lightBulbState.isOn,lightBulbState.hue,lightBulbState.sat,lightBulbState.bri ); };const upDateBri = (value) => {setLightBulbState((prev) => {return { ...prev, bri: Math.floor(value) }; });controlLight(lightId,lightBulbState.isOn,lightBulbState.hue,lightBulbState.sat,lightBulbState.bri ); };const upDateSat = (value) => {setLightBulbState((prev) => {return { ...prev, sat: Math.floor(value) }; });controlLight(lightId,lightBulbState.isOn,lightBulbState.hue,lightBulbState.sat,lightBulbState.bri ); };return (<SafeArea><View style={styles.container}><Text>{lightbulb.name} </Text><View style={styles.sliderContainer}><Sliderstyle={{ width: 300, height: 40 }}minimumValue={0}maximumValue={254}minimumTrackTintColor="#FFFFFF"maximumTrackTintColor="#000000"value={lightbulb.state.sat}onValueChange={upDateSat}/><Text>Saturation: {lightBulbState.sat || lightbulb.state.sat}</Text></View><View style={styles.sliderContainer}><Sliderstyle={{ width: 300, height: 40 }}minimumValue={0}maximumValue={254}minimumTrackTintColor="#FFFFFF"maximumTrackTintColor="#000000"value={lightbulb.state.bri}onValueChange={upDateBri}/><Text>Brightness: {lightBulbState.bri || lightbulb.state.bri}</Text></View><View style={styles.btnContainer}><Buttontitle={lightBulbState.isOn ? "currently on" : "currently off"}onPress={toggleLight}/></View></View></SafeArea> );`
The three logs that I have bold faced are logging 3 times each. Here is the data from the logs:
93 71 from params
Object {
"bri": 71,
"hue": 8417,
"isOn": true,
"sat": 93,
}
8417 93 71 from control light
93 71 from params
Object {
"bri": 71,
"hue": 8417,
"isOn": true,
"sat": 0,
}
8417 0 71 from control light
93 71 from params
Object {
"bri": 0,
"hue": 8417,
"isOn": true,
"sat": 0,
}
I would expect to only see the logs once instead I get them 3 times. Also even if it did log more than once I would expect the data to remain the same every time but as you can see on the second iteration I'm losing "sat" and on the third I'm losing "bri"
What could be causing this behavior? I hope someone can point me in the right direction.
Thanks!

It's pretty normal to have re-renders. But if you want to avoid them, such as for performance reasons (which tends to be a good idea), you need to identify why it's re-rendering.
For this, you can use the tool Why Did You Render. It will compare props and see which props have changed. In this case, it's probably lightbulb, because as it's an object, during each render tick lightbulb (old) !== lightbulb (new). This is because object instances are not equal to each other ({} !== {}). You can get around this by passing lightbulb props as individual props (i.e. LightDetailScreen = ({hue, sat, bri, on}) => etc...). You may also need to wrap your LightDetailScreen in React.memo, so that the same component instance is returned when the props are the same.
That said, the (probably) only reason it's re-rendering in the first place is because a higher up component is re-rendering as well, so you could also investigate your root component and see why it's mounting 3 times.
If you'd like more ideas, post more code and maybe we can pinpoint where the extra renders are coming from.

Related

Populte WYSIWYG editor after react native fetch

I am trying to incorporate this WYSIWYG package into my react native project (0.64.3). I built my project with a managed workflow via Expo (~44.0.0).
The problem I am noticing is that the editor will sometimes render with the text from my database and sometimes render without it.
Here is a snippet of the function that retrieves the information from firebase.
const [note, setNote] = useState("");
const getNote = () => {
const myDoc = doc(db,"/users/" + user.uid + "/Destinations/Trip-" + trip.tripID + '/itinerary/' + date);
getDoc(myDoc)
.then(data => {
setNote(data.data()[date]);
}).catch();
}
The above code and the editor component are nested within a large function
export default function ItineraryScreen({route}) {
// functions
return (
<RichEditor
onChange={newText => {
setNote(newText)
}}
scrollEnabled={false}
ref={text}
initialFocus={false}
placeholder={'What are you planning to do this day?'}
initialContentHTML={note}
/>
)
}
Here is what it should look like with the text rendered (screenshot of simulator):
But this is what I get most of the time (screenshot from physical device):
My assumption is that there is a very slight delay between when the data for the text editor is actually available vs. when the editor is being rendered. I believe my simulator renders correctly because it is able to process the getNote() function faster.
what I have tried is using a setTimeOut function to the display of the parent View but it does not address the issue.
What do you recommend?
I believe I have solved the issue. I needed to parse the response better before assigning a value to note and only show the editor and toolbar once a value was established.
Before firebase gets queried, I assigned a null value to note
const [note, setNote] = useState(null);
Below, I will always assign value to note regardless of the outcome.
if(data.data() !== undefined){
setNote(data.data()[date]);
} else {
setNote("");
}
The last step was to only show the editor once note no longer had a null value.
{
note !== null &&
<RichToolbar
style={{backgroundColor:"white", width:"114%", flex:1, position:"absolute", left:0, zIndex:4, bottom: (toolbarVisible) ? keyboardHeight * 1.11 : 0 , marginBottom:-40, display: toolbarVisible ? "flex" : "none"}}
editor={text}
actions={[ actions.undo, actions.setBold, actions.setItalic, actions.setUnderline,actions.insertLink, actions.insertBulletsList, actions.insertOrderedList, actions.keyboard ]}
iconMap={{ [actions.heading1]: ({tintColor}) => (<Text style={[{color: tintColor}]}>H1</Text>), }}
/>
<RichEditor
disabled={disableEditor}
initialFocus={false}
onChange={ descriptionText => { setNote(descriptionText) }}
scrollEnabled={true}
ref={text}
placeholder={'What are you planning to do?'}
initialContentHTML={note}
/>
}
It is working properly.

Creating a checkbox group with React Native

Good Morning! I am wanting to create a selection box where the user has several options of items to choose from and when clicking on a button, it triggers a function that shows all the values that the user chose in the form of an array, json or even arrays ( hard task).
In the React Native documentation, only simple examples of checkboxes using the component are provided and I wanted to go much further than the documentation provides me. What are the possible solutions to this problem? (from a simpler example to an advanced one) and what (s) ways can I explore this problem in order to solve it in the most practical and uncomplicated way?
Definitions and examples of official documentation:
https://reactnative.dev/docs/checkbox/ (CheckBox)
https://reactnative.dev/docs/button/ (Button)
With this problem, another one came up: build an application where the user selects shopping options (items) and a subtotal is displayed in the lower corner of the application as he selects or deselects the items he is going to buy, and there is also an option to reset the subtotal by returning it to the zero value.
From the problem mentioned at the beginning, what are the possible solutions to create this application previously mentioned in a practical and simple way?
Multi Checkbox example ( Updated with Hook )
export const Example = () => {
const [checkboxes, setCheckboxes] = useState([{
id: 1,
title: 'one',
checked: false,
}, {
id: 2,
title: 'two',
checked: false,
}]);
const onButtonPress = () => {
const selectedCheckBoxes = checkboxes.find((cb) => cb.checked === true);
// selectedCheckBoxes will have checboxes which are selected
}
const toggleCheckbox = (id, index) => {
const checkboxData = [...checkboxes];
checkboxData[index].checked = !checkboxData[index].checked;
setCheckboxes(checkboxData);
}
render(){
const checBoxesView = checkboxes.map((cb, index) => {
return (
<View style={{flexDirection:"row"}}>
<Checkbox
key={cb.id}
checked={cb.checked}
onPress={() => toggleCheckbox(cb.id, index)} />
<Text>{cb.title}</Text>
</View>
);
});
return (
<View>
{ checBoxesView }
<Button onPress={onButtonPress} title="Click" />
</View>
);
}
}

React Native: Checkbox List Structure

A user object has an array prop schools that references one or more school objects. I would like to use a <List> with <CheckBox> to mutate the schools array.
I load the user object into the view, and I load the listOfSchools (from the application state) to generate the checkbox list:
<List data={listOfSchools} keyExtractor={ item=> item._id } renderItem={({item})=>renderItem(item)} />
The renderItem function:
const renderItem = (school) => {
return <ListItem
title={school.name}
accessory={()=>renderAccessory(school)}
/>
};
The renderAccessory function:
const renderAccessory = (school) => {
return <CheckBox checked={() => checkSchool(school._id)} onChange={()=>changeSchool(school._id)} />
}
The checkSchool function returns boolean on if the school._id is referenced in the user.schools array. The changeSchool function adds or removes the school._id from the users.schools array.
The changeSchool function:
const changeSchool = (schoolId) => {
let checked = checkSchool(schoolId);
if (!checked) {
// add schoolId to user.schools
} else {
// remove schoolId from user.schools
}
}
This drastically does not work. It appears that no matter what I use to mutate the state, the checkboxes never update, nor does the user.schools array mutate.
What is the proper way to structure such a design goal?
Assuming that you use UI Kitten, I can see that you got the checked prop value wrong for the CheckBox component.
UI Kitten CheckBox reference
The checked prop needs to be a boolean not a Callable as you have it there
I would try to change the code like this:
const renderAccessory = (school) => {
const isChecked = checkSchool(school._id);
return <CheckBox checked={isChecked} onChange={()=>changeSchool(school._id)} />
}
Let me know if that helped.
While trying various solutions i can conclude few things here:
With the solution given by #Cornel Raiu the checked and unchecked flags are getting correctly calculated however, the display was not correct with the state of checked/unchecked
I replaced Checkbox with Toggle, just to be sure that it works with iOS too
PROBLEM that i faced still is that, even the State of item getting toggled is correctly populating it was getting reset
The outside container of Toggles is List and ListItem,
OBSERVATION is that the Press event on List was actually getting the Checkbox/Toggle into correct Display State...
SOLUTION:
After longer time of research and experiments I got my thing working with following approach -
I maintained separate collection of Checked Items
There is already a state of Collection of master items, as input to List
Every time the Checkbox/Toggle is clicked, the master list of Data is cloned and copied back to its state
This was triggering the slight re-render of component and thing is working as expected.
const [cashTransactions, setCashTransactions] = useState([]); // master data
const [selectedTransactions, setSelectedTransactions] = useState([]); // selected data
const renderItem = ({ item, index }) => (
<ListItem
title={'('+item.id + ') ' + item.firstName + ' ' + item.lastName}
description={Moment(item.createdOn).format('yyyy-MM-DD hh:mm:ss')}
accessoryLeft={selectedTransactions.includes(item.id) ? RadioOnIcon : RadioOffIcon}
accessoryRight={() => checkBoxSpace(item)}
/>
);
const checkBoxSpace = (item) => {
let itemChecked = selectedTransactions.includes(item.id);
return (
<View style={styles.actionContainer}>
<Button size='tiny' status='basic' accessoryLeft={rupeeSymbol}>{item.amount}</Button>
<Toggle checked={itemChecked} status='primary' size='small' onChange={checked => checkboxChecked(item, checked)}></Toggle>
</View>
)
}
const checkboxChecked = (item, checked) => {
console.log('Item -' + item.id + ' ' + checked);
if (checked) {
if (!selectedTransactions.includes(item.id)) {
selectedTransactions.push(item.id);
}
} else {
if (selectedTransactions.includes(item.id)) {
selectedTransactions.pop(item.id);
}
}
console.log('selectedTransactions ' + JSON.stringify(selectedTransactions));
// This is the thing i applied to get it done.
const cloned = [...cashTransactions];
setCashTransactions(cloned);
}
// View
<List
style={styles.container}
data={cashTransactions}
renderItem={renderItem}
/>

React Native for loop with Array not doing what I expected

I'm trying out some things in React Native for the first time and i'm trying to roll 3 dices (text based for now).
I'm using a for loop to go over an array of the 3 dices. However i'm only seeing one dice text being updated (the 3rd one).
Also when doing some alerts to check what's going on within that for loop, i'm seeing unexpected things? the first alert says 2, the second alert says 1 and then it usually no longer alerts, seldom it also alerts a third time with a 0.
My code so far:
(file: Game.js)
import React from 'react'
import { StyleSheet, Image, Text, View, Button } from 'react-native'
export default class Home extends React.Component {
state = {
dices: [null, null, null],
rollsLeft: 3,
keepDices: [false, false, false],
}
//When this component is mounted (loaded), do some defaults
componentDidMount() {
}
//Roll dices
rollDices = () => {
for(let i = 0; i < 3; i++){
alert('for loop at ' + i);
//Math random to get random number from rolling the dice
randomNumber = Math.floor(Math.random() * 6) + 1;
//Check if the user wanted to keep a dice's value before reassigning a new value
if(this.state.keepDices[i] === false){
//User want to roll this dice, assign new random number to it
//this.setState.dices[i] = randomNumber;
let newDices = [ ...this.state.dices ];
newDices[i] = randomNumber;
this.setState({ dices : newDices });
}
}
//Deduct 1 roll from total
this.setState.rollsLeft--;
//TODO: Check if rolls equals 0, then make player2 active!
}
render() {
return (
<View style={styles.container}>
<Text> {this.state.dices[0]} ONE </Text>
<Text> {this.state.dices[1]} TWO</Text>
<Text> {this.state.dices[2]} THREE</Text>
<Text>Turns left: {this.state.rollsLeft} </Text>
<Button
title="Roll 🎲"
onPress={this.rollDices} />
</View>
)
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center'
},
})
In React Native the setState function is asynchronous.
Meaning that this.setState({ dices : newDices }); can end up setting dices to different values depending on which finishes first.
If you want to control what happens after you use setState, you can call a function after the set is done like this
this.setState({dices: newDices}, () => {
// Do something here.
});
There is some really useful information on calling function after the setState here: Why is setState in reactjs Async instead of Sync?
and some good explanations of how setState in react works and how to get around it here: https://medium.com/#wereHamster/beware-react-setstate-is-asynchronous-ce87ef1a9cf3
Together with DCQ's valuable input for async setStates bundling I also noticed that i'm always resetting the copied dice array within the for loop and thus only saving my last dice correctly.
Next up the for loop was actually counting right from 0 to 2 however the alert boxes don't interrupt the code as i'm used to in the browser therefore it looked a bit off. When doing console.log (which is also cleaner and more correct logging) I noticed things did went right there.

React Native Retrieve Actual Image Sizes

I would like to be able to know the actual size of a network-loaded image that has been passed into <Image /> I have tried using onLayout to work out the size (as taken from here https://github.com/facebook/react-native/issues/858) but that seems to return the sanitised size after it's already been pushed through the layout engine.
I tried looking into onLoadStart, onLoad, onLoadEnd, onProgress to see if there was any other information available but cannot seem to get any of these to fire. I have declared them as follows:
onImageLoadStart: function(e){
console.log("onImageLoadStart");
},
onImageLoad: function(e){
console.log("onImageLoad");
},
onImageLoadEnd: function(e){
console.log("onImageLoadEnd");
},
onImageProgress: function(e){
console.log("onImageProgress");
},
onImageError: function(e){
console.log("onImageError");
},
render: function (e) {
return (
<Image
source={{uri: "http://adomain.com/myimageurl.jpg"}}
style={[this.props.style, this.state.style]}
onLayout={this.onImageLayout}
onLoadStart={(e) => {this.onImageLoadStart(e)}}
onLoad={(e) => {this.onImageLoad(e)}}
onLoadEnd={(e) => {this.onImageLoadEnd(e)}}
onProgress={(e) => {this.onImageProgress(e)}}
onError={(e) => {this.onImageError(e)}} />
);
}
Thanks.
Image component now provides a static method to get the size of the image. For example:
Image.getSize(myUri, (width, height) => {this.setState({width, height})});
You can use resolveAssetSource method from the Image component :
import picture from 'pathToYourPicture';
const {width, height} = Image.resolveAssetSource(picture);
This answer is now out of date. See Bill's answer.
Image.getSize(myUri, (width, height) => { this.setState({ width, height }) });
Old Answer (valid for older builds of react native)
Ok, I got it working. Currently this takes some modification of the React-Native installation as it's not natively supported.
I followed the tips in this thread to enabled me to do this.
https://github.com/facebook/react-native/issues/494
Mainly, alter the RCTNetworkImageView.m file: add the following into setImageURL
void (^loadImageEndHandler)(UIImage *image) = ^(UIImage *image) {
NSDictionary *event = #{
#"target": self.reactTag,
#"size": #{
#"height": #(image.size.height),
#"width": #(image.size.width)
}
};
[_eventDispatcher sendInputEventWithName:#"loaded" body:event];
};
Then edit the line that handles the load completion:
[self.layer removeAnimationForKey:#"contents"];
self.layer.contentsScale = image.scale;
self.layer.contents = (__bridge id)image.CGImage;
loadEndHandler();
replace
loadEndHandler();
with
loadImageEndHandler(image);
Then in React-Native you have access to the size via the native events. data from the onLoaded function - note the documentation currently says the function is onLoad but this is incorrect. The correct functions are as follows for v0.8.0:
onLoadStart
onLoadProgress
onLoaded
onLoadError
onLoadAbort
These can be accessed like so:
onImageLoaded: function(data){
try{
console.log("image width:"+data.nativeEvents.size.width);
console.log("image height:"+data.nativeEvents.size.height);
}catch(e){
//error
}
},
...
render: function(){
return (
<View style={{width:1,height:1,overflow='hidden'}}>
<Image source={{uri: yourImageURL}} resizeMode='contain' onLoaded={this.onImageLoaded} style={{width:5000,height:5000}} />
</View>
);
}
Points to note:
I have set a large image window and set it inside a wrapping element of 1x1px this is because the image must fit inside if you are to retrieve meaningful values.
The resize mode must be 'contain' to enable you to get the correct sizes, otherwise the constrained size will be reported.
The image sizes are scaled proportionately to the scale factor of the device, e.g. a 200*200 image on an iPhone6 (not 6 plus) will be reported as 100*100. I assume that this also means it will be reported as 67*67 on an iPhone6 plus but I have not tested this.
I have not yet got this to work for GIF files which traverse a different path on the Obj-C side of the bridge. I will update this answer once I have done that.
I believe there is a PR going through for this at the moment but until it is included in the core then this change will have to be made to the react-native installation every time you update/re-install.
TypeScript example:
import {Image} from 'react-native';
export interface ISize {
width: number;
height: number;
}
function getImageSize(uri: string): Promise<ISize> {
const success = (resolve: (value?: ISize | PromiseLike<ISize>) => void) => (width: number, height: number) => {
resolve({
width,
height
});
};
const error = (reject: (reason?: any) => void) => (failure: Error) => {
reject(failure);
};
return new Promise<ISize>((resolve, reject) => {
Image.getSize(uri, success(resolve), error(reject));
});
}