Toggle button not toggling for the first time - javascript

My app is simple: one button with one state toggleButton. In constructor toggleButton is set to the default value false. When I press the button, the app will start record some sensors and console.log their data to the chrome debugger screen.
constructor(props) {
super(props);
this.state = {
toggleButton: false
};
}
recordSensors() {
let toggleButton = !this.state.toggleButton;
this.setState({ toggleButton });
if (this.state.toggleButton) {
// start & record some sensors data
} else {
// stop recording
}
}
render() {
return (
<View style={styles.container}>
<TouchableOpacity
style={styles.toggleButton}
onPress={() => this.recordSensors()}
>
<Text style={styles.buttonText}>
{this.state.toggleButton ? 'Stop' : 'Start'}
</Text>
</TouchableOpacity>
<Text>
{this.state.toggleButton ? 'Recording...' : null}
</Text>
</View>
);
}
The weird thing is, the first time I press the button, its text changed to Stop and the Recording... text appeared but the app didn't record sensors data. When I press the button again (the second time), then it now records.
But if I change if (this.state.toggleButton) to if (toggleButton) then it works fine. I can't understand the logic of it anymore. Can you guys help?

You are using
let toggleButton = !this.state.toggleButton;
Where toggleButton has inverse value of this.state.toggleButton
And, say if, this.state.toggleButton is false then toggleButton will have true as its value. So, the condition you are specifying is totally different here
if (this.state.toggleButton) //if(false)
And when you do
if(toggleButton) //if(true)
So, notice that condition when you have this.state.toggleButton as false or vice-versa

Your problem:
onPress={() => this.recordSensors()}
Fix:
onPress={() => this.recordSensors}
Here is your logic:
In your constructor:
toggleButton = false;
In render:
onPress={() => this.recordSensors()}
Which calls:
//currently this.state.toggleButton == false;
let toggleButton = !this.state.toggleButton; // let toggleButton=true;
this.setState({ toggleButton }); // now state.toggleButton = true;
So now when you click your button, you care call recordSensors() for a second time:
//currently this.state.toggleButton == true;
let toggleButton = !this.state.toggleButton; // let toggleButton =false;
this.setState({ toggleButton }); // now state.toggleButton == false;

Related

React Native check context value on click test case

I have following context
Home.tsx
export const ThemeContext = React.createContext(null)
const Home = () => {
const { width } = Dimensions.get("window")
const [theme, setTheme] = React.useState({
active: 0,
heightOfScrollView: 0,
profileWidth: width * 0.2,
scrolledByTouchingProfile: false
})
const horizontalScrollRef = React.useRef<ScrollView>()
const verticalScrollRef = React.useRef<ScrollView>()
return (
<>
<SafeAreaView style={styles.safeAreaContainer} />
<Header title="Contacts" />
<ThemeContext.Provider value={{ theme, setTheme }}>
In component A, I have a button which changes in the context
const onProfileTouched = (index: number) => {
setTheme({ ...theme, active: index });
};
This leads to an image being active
const ImageCircle = ({ active, uri }: Props) => {
return (
<View
style={
active
? { ...styles.parentView, ...styles.active }
: { ...styles.parentView }
}>
<Image source={uri} width={30} height={30} />
</View>
);
};
Now, I want to write a test case (I haven't written a test case before) that confirms that the state has actually changed or perhaps an active border is added to the image
I added a testId to my button which I used to fire an event
it('changes active on profile clicked', () => {
const { getByTestId } = render(<Home />);
fireEvent.press(getByTestId('HScroll3.button'));
});
Now, I am unsure, how to grab the value of context or change in style so as I can confirm that indeed the component for which the button is pressed is active
I am using import {render, fireEvent} from '#testing-library/react-native' but open to change.
With testing-library you want to check the visual output, not the internal state. This way your tests are much more valuable, because the end user doesn't care if you're using context, state or anything else, they care if the button has the "active" state. So if at some point you'll decide to change your mind and refactor theming completely, your test will still give you value and confidence.
I would suggest to install #testing-library/jest-native, you just have to add this setupFilesAfterEnv": ["#testing-library/jest-native/extend-expect"]
to your Jest config or import that extend-expect file in your test setup file.
Once you have it set up, you're ready to go.
I don't know what's in your styles.active style, but let's assume it's e.g. { borderColor: '#00ffff' }.
Add a testID prop to the View in ImageCircle component, e.g. testID="imageCircleView". Then to test if everything works as you'd expect, you just have to add this to your test:
expect(getByTestId('imageCircleView')).toHaveStyle({ borderColor: '#00ffff' });
And that's it.

Buttons and touchables on React Native lag with delay

I'm tying to create a basic e-shop in my application and it works fine. Every item in flatlist has 3 buttons ( TouchableOpacity ) to set quantity.
Those buttons are very slow: 1 second between clock and re-rendering . It looks like a long press but it's a simple click: this is a simple video to show you
And this is the code in detail:
class Shop extends React.Component {
...
selectItem = (item, typeButton) => {
if (item.qte >= 0) {
switch (typeButton) {
case 'plus':
if (parseFloat(item.EshopPrice) <= parseFloat(this.state.score)) {
this.setState({
score: parseFloat(this.state.score).toFixed(2) - parseFloat(item.EshopPrice).toFixed(2),
})
const actionSum = { type: "INCREASE_SUM", value: item }
this.props.dispatch(actionSum)
} else {
this.showToast();
}
break;
case 'minus':
if (this.props.totaleQte > 0) {
item.qte = item.qte - 1
this.setState({
score: Number(parseFloat(item.EshopPrice).toFixed(2)) + this.state.score,
})
const actionSumMoin = { type: "DECREASE_SUM", value: item }
this.props.dispatch(actionSumMoin)
}
break;
case 'plus+':
if (parseFloat(item.EshopPrice) <= parseFloat(this.state.score)) {
item.qte = item.qte + 1
this.setState({
score: parseFloat(this.state.score).toFixed(2) - parseFloat(item.EshopPrice).toFixed(2),
})
const actionSum = { type: "SET_CURRENTSALE", value: item }
this.props.dispatch(actionSum)
} else {
this.showToast();
}
break;
default:
break;
}
}
};
...
render ()
...
return (...)
}
I call this function in a functional component in the same file which is the renderItem of a flatlit :
renderItem = ({ item }) => {
return (
<View style={StylesGift.buttonsContainer}>
{
item.qte === 0 ?
<TouchableOpacity
onPress={() => this.selectItem(item, 'plus+')}>
<Text style={[StylesGift.itemQte, StylesGift.roundItemQte]}>+</Text>
</TouchableOpacity>
:
<View style={StylesGift.buttonsQteContainer}>
<TouchableOpacity onPress={() => this.selectItem(item, 'minus')}>
<Text style={[StylesGift.itemQte, StylesGift.roundItemQte]}>-</Text>
</TouchableOpacity>
<Pressable
onPress={() => this.showModal(true, item)}>
<Text style={StylesGift.itemQte}>{item.qte}</Text>
</Pressable>
<TouchableOpacity onPress={() => this.selectItem(item, 'plus')}>
<Text style={[StylesGift.itemQte, StylesGift.roundItemQte]}>+</Text>
</TouchableOpacity>
</View>
}
</View>
)
}
I think the problem is in the setState({score: ...}) and dispatching action to redux, because when I remove them all or remove one of them, the click becomes very fast and smooth.
This is the treatment on the reducer:
case 'INCREASE_SUM':
const productShopIndex = state.Data.findIndex(item => item.ProductID === action.value.ProductID)
state.Data[productShopIndex].qte = state.Data[productShopIndex].qte + 1
nextState = {
...state,
sum: state.sum + parseFloat(action.value.EshopPrice),
}
return nextState || state
case 'DECREASE_SUM':
nextState = {
...state,
totaleQte: action.value.qte === 0 ? state.totaleQte - 1 : state.totaleQte,
sum: state.sum - parseFloat(action.value.EshopPrice),
}
return nextState || state
Actually the problem is you are directly dispatching the global actions while pressing the buttons. The side effect of this is when you press + or -, the reducer will take some time to do computation and change the state(Thats why theres delay there since the JS thread is blocked). The simplest solution to this is for every counter, make the increment or decrement as local state and inside useEffect(or componentDidUpdate) sync that count with reducer after some debounce. The flow is:
Store the count value in local state inside the counter
Make debounce of like 500ms so that when user presses any button within this time,it will ignore the last count and only update when user leaves counter after the debounce time.
And sync with the global reducer after the debounce.
I came across similar situation recently. So hope this helps.

How to refer to a single element of a mapped array to render a View

I've got an array of object. I mapped it to render it like a list with a label and an icon button.
I need to show another View below the element of the row when I hit the button.
The array of object is a let variable. I would have like to make it as a state (because of the re-rendering when a function setState is executed), but I need it as a let variable because they are datas coming from a navigation route
My idea was to add a flag in every object of the array to indicate when the View has to be rendered. This would have been false at the beginning and true when I hit the button, but nothing happen. While if don't use a flag when I hit the button, the View is rendered in every row
state={
visible:false
}
getNewView = (item) => {
if (this.state.visible && item.showViewBelow)
return (
<View>
<HiddenViewComponent/>
</View>
)
};
btnHandler = (item) => {
item.showViewBelow = true;
this.setState({visible:true})
}
render(){
const {navigation} = this.props;
const arrayNav = navigation.getParam('data', '');
let array = [
{
id:'0',
label: arrayNav.label1,
showViewBelow: false
},
{
id:'1',
label: arrayNav.label2,
showViewBelow: false
},
{
id:'2',
label: arrayNav.label3,
showViewBelow: false
}
]
return(
<View>
array.map((item, key)=> { return
<View key = {key} style={{marginHorizontal: 25}}>
<Text>{item.label}</Text>
<TouchableOpacity onPress={this.btnHandler(item)}>
<Image source=(blablabla)/>
</TouchableOpacity>
{this.getNewView(item)}
</View>)
)}
</View>
)
}
So, when i click in a row of the list, I want a new view to be showed below THIS row.
I tried to switch to true the flag in one of the object and leave it to false in the others. It works fine on that line! When I click the button in that row, the new view is correctly rendered below that row.
I tried also using a Flatlist and put some extradata with a state variable to be updated with a setState after the return of the new view. (just to try to re-render the page).
Still, I tried to put this.forceUpdate() in the getNewView method. Nothing happen. Nothing is rendered
So I think the problem is I don't re-render the whole screen, because having the object as let variable, this doesn't happen.
Have any ideas?
You can create method in constant utility to define defaultArray like
getDefaultArray(arrayNav){
return [
{
id:'0',
label: arrayNav.label1,
showViewBelow: false
},
{
id:'1',
label: arrayNav.label2,
showViewBelow: false
},
{
id:'2',
label: arrayNav.label3,
showViewBelow: false
}
];
}
In your component code
import Constants from "./Constants"
ComponentDidMount(){
const {navigation} = this.props;
const arrayNav = navigation.getParam('data', '');
this.state={
visible:false,
array = Constants.getDefaultArray(arrayNav);
}
}
getNewView = (item) => {
if (this.state.visible && item.showViewBelow)
return (
<View>
<HiddenViewComponent/>
</View>
)
};
btnHandler = (item, index) => {
const {navigation} = this.props;
const arrayNav = navigation.getParam('data', '');
let newData = Constants.getDefaultArray(arrayNav);
newData[index].showViewBelow = true;
this.setState({visible:true , array:newData})
}
render(){
return(
<View>
this.state.array.map((item, key)=> { return
<View key = {key} style={{marginHorizontal: 25}}>
<Text>{item.label}</Text>
<TouchableOpacity onPress={this.btnHandler(item,key)}>
<Image source=(blablabla)/>
</TouchableOpacity>
{this.getNewView(item)}
</View>)
)}
</View>
)
}

Changing state for pressed item only

In my react-native app I have a function that changes the state of the pressed item from false to true, but the issue is that it's changing the state for all of the items not only the pressed one, i want when i press to change it for the pressed one only inside my FlatList, here is the code:
Initial state:
state={pressed: false}
Function:
changeItem = async item => {this.setState({ pressed: true });};
Rendering item and binding the function:
_renderItem({ item, index }) {
<TouchableOpacity onPress={this.changeItem.bind(this, item)}>
<Text> Click me </Text>
</TouchableOpacity>
);
}
FlatList:
<FlatList data={this.state.items}
keyExtractor={this._keyExtractor.bind(this)}
renderItem={this._renderItem.bind(this)}/>
The problem here is that you have a list of items, but all of then have the same state.
You need a list of items (an array) but you also need an array of the items state.
So instead of state = { pressed: false } you need state = { pressed: []}
And when rendering the items you need to pass the index of the pressed button:
_renderItem({ item, index }) {
return this.state.pressed[index] &&
<TouchableOpacity onPress={this.changeItem.bind(this, item, index)}>
<Text> Click me </Text>
</TouchableOpacity>
}
And Update the state only in the selected index
changeItem = async (item, index) => {
let itensPressed = this.state.pressed
itensPressed[index] = itensPressed[index] ? false : true
this.setState({ pressed: itensPressed })
}
But there is something better than doing this.
I see that you are getting the items from the state so maybe you want to update the items array an not create another variable.
This depends on how is your this.state.items is coming and if you can or can't have the isPressed value in that array.
If you show how your items is coming, I can create a better answer.
If you have multiple button that have its own state. you need to make an array pressed boolean, in state so that each element in an array has its own pressed state.
Do it like this.
import React, {Component} from 'react';
import { FlatList, TouchableOpacity, Text} from 'react-native';
sampleData = [
{key: 0, pressed: false},
{key: 1, pressed: false},
{key: 2, pressed: false}
]
export default class Example extends Component {
state={
data: sampleData
}
componentDidUpdate(){
console.log(this.state.data)
}
changeItem(item)
{
this.setState( (prevState) => {
prevState.data[item.key] = { ...item, pressed: !item.pressed}
return{
...prevState,
data: [...prevState.data]
}
});
};
_keyExtractor = (item, index) => item.key;
_renderItem({ item, index }) {
return(
<TouchableOpacity onPress={this.changeItem.bind(this,item)}>
<Text> Click me </Text>
</TouchableOpacity>
)
}
render() {
const {data} = this.state
return (
<FlatList
data={data}
keyExtractor={this._keyExtractor}
renderItem={this._renderItem.bind(this)}
/>
)
}
}
i just create an array of object contains key and pressed property and passed it to state.data
check console log, to see the output state.

Disable buttons from parent

I am creating a Quiz app for the sake of learning React Native.
I want that when a user presses an answer, all buttons should be disabled. I have no idea how to do this, I have tried all different approaches, like changing props of the buttons from the parent, setting state from the parent etc. I just can't figure it out. I can make the clicked button disabled, but that doesn't help since the other buttons are still clickable.
Parent
class Container extends Component {
state = { currentQuestion: questions[0] }
buttons = new Array();
componentWillMount() {
this.makeButtons();
}
makeButtons() {
for (let i = 0; i < 4; i++) {
const isCorrect = (i === 0); //the first answer is correct, this is how I keep track
const btn = (
<Button
key={i}
title={this.state.currentQuestion[i]}
isCorrect={isCorrect}
/>
);
this.buttons.push(btn);
}
shuffle(this.buttons);
}
render() {
return (
<View style={containerStyle}>
<Text style={textStyle}>
{this.state.currentQuestion.title}
</Text>
{this.buttons}
</View>
);
}
}
Button
class Button extends Component {
state = { color: "rgb(0,208,196)" };
handleEvent() {
const newColor = (this.props.isCorrect) ? "green" : "red";
this.setState({ color: newColor });
this.props.onPress();
}
renderButton() {
return (
<TouchableOpacity
style={buttonStyle}
onPress={this.handleEvent.bind(this)}
disabled={this.props.disabled}
>
<Text style={textStyle}>
{this.props.title}
</Text>
</TouchableOpacity>
);
}
render() {
return this.renderButton();
}
}
You are creating your button components within an instance variable once when the parent component loads, but never re-rendering them. This is an anti-pattern of React. Ideally, your components should all be rendered within render(), and their props should be computed from state, so you only need to worry about updating the state correctly and all your components render properly.
In this case, you should construct the data for your buttons at component load, save your button data within state, and render your buttons within render(). Add a "disabled" state to your Button component, and when a user presses one of the buttons, use a callback to set "disabled" state in the parent component, and all your buttons will re-render to be properly disabled.

Categories

Resources