Mobx React `autorun` called more times on every change - mobx

I've setup a simple app store with a single numeric value, which I increment on every button click. My UI is simple: a single app <div> which contains a MyChild component that renders the number next to an increment button.
The app's autorun seems to behave correctly BUT every time I increment the value, MyChild's autorun fire extra times i.e. on page load it fires once. If I click the button, it fires twice. I click again, it fires 3 times, and so on. I expect that on every increment, autorun would fire once. What am I missing here?
Code is available on CodeSandbox
Here it is here as well:
import "./styles.css";
import * as React from "react";
import { observer } from "mobx-react-lite";
import { action, autorun, makeAutoObservable } from "mobx";
class AppStore {
v;
constructor() {
this.v = 0;
makeAutoObservable(this);
}
}
const appStore = new AppStore();
const MyChild = observer(() => {
console.log("MyChild render", appStore.v);
autorun(() => { // <------------------------ this gets fired extra times
console.log("mychild autorun " + appStore.v);
});
return (
<div style={{ backgroundColor: "lightBlue" }}>
mychild {appStore.v}
{": "}
<button
onClick={action(() => {
appStore.v += 1;
})}
>
INC
</button>
</div>
);
});
export default observer(function App() {
console.log("app render");
autorun(() => {
console.log("app autorun " + appStore.v);
});
return (
<>
<div style={{ backgroundColor: "gray", padding: "10px" }}>
main
<MyChild />
</div>
</>
);
})

I have found the reason (I'm new to Mobx-React, guess I should have figured it out)
According to this tip, I need to setup autorun inside a useEffect that happens on first render. I changed all my autoruns to:
React.useEffect(() => {
return autorun(...);
}, []);
and now they get fired once every render.

Related

Is it possible to wait for a component to render? React Testing Library/Jest

I have a component. It has a button. Upon pressing the button, I am changing the style of the button text (color) using setState function. When I am testing the changed component, the test is failing because the change happens asynchronously. I want to do something as is given here (https://testing-library.com/docs/dom-testing-library/api-async/)
const button = screen.getByRole('button', { name: 'Click Me' })
fireEvent.click(button)
await screen.findByText('Clicked once')
fireEvent.click(button)
await screen.findByText('Clicked twice')
But rather than waiting for the text to change. I want to wait for the text color to change. Thanks
This is the code for my button
<Button onPress = {() => {this.setState({state : 1});}}>
<Text style = {style}>Button Text</Text>
</Button>
So when this button is pressed. state is set to 1. And in render :
if(this.state.state === 1) style = style1
else style = style2;
But it can be seen from logs that render is called after the test checks for the styles. So How can I wait for the render to complete before checking if the font color has been changed?
Here is the testing code
test('The button text style changes after press', () => {
const {getByText} = render(<Component/>);
fireEvent.press(getByText('button'));
expect(getByText('button')).toHaveStyle({
color : '#ffffff'
});
})
It looks like you have a custom button, not a native button. I'm guessing your component is something like this:
import React from "react";
import {Text, TouchableOpacity} from "react-native";
const Button = ({pressHandler, children}) => (
<TouchableOpacity onPress={pressHandler}>
{children}
</TouchableOpacity>
);
const ColorChangingButton = ({text}) => {
const [color, setColor] = React.useState("red");
const toggleColor = () => setTimeout(() =>
setColor(color === "green" ? "red" : "green"), 1000
);
return (
<Button pressHandler={toggleColor}>
<Text style={{color}}>{text}</Text>
</Button>
);
};
export default ColorChangingButton;
If so, you can test it with waitFor as described here:
import React from "react";
import {
fireEvent,
render,
waitFor,
} from "#testing-library/react-native";
import ColorChangingButton from "../src/components/ColorChangingButton";
it("should change the button's text color", async () => {
const text = "foobar";
const {getByText} = render(<ColorChangingButton text={text} />);
fireEvent.press(getByText(text));
await waitFor(() => {
expect(getByText(text)).toHaveStyle({color: "green"});
});
});
For a native button which has rigid semantics for changing colors and doesn't accept children, instead using title="foo", a call to debug() shows that it expands to a few nested elements. You can use
const text = within(getByRole("button")).getByText(/./);
expect(text).toHaveStyle({color: "green"});
inside the waitFor callback to dip into the button's text child and wait for it to have the desired color.
I used the same packages/versions for this post as shown in React Testing Library: Test if Elements have been mapped/rendered.
You can try
<Text style = {this.state.state === 1 ? style1 : style2}>Button Text</Text>
This will consequently lead to the style being defined all time. So you don't have to wait for the setState to complete.
Edit
You can use the callback provided by setState function to perform your tests for styles.
this.setState({
state : 1
} , () => {
//this is called only after the state is changed
//perform your test here
})

React native with redux is laggy on dispatch

In my app, I have a function which calls every 2s a bluetooth command to ask the current temperature of a device with a setInterval function.
The bluetooth response is given by monitored function. I use react native-ble-plx library for that.
I have no problem with this process.
The temperature is returned via a property which is dispatched via redux in an action file.
But when I "dispatch" (via redux) the function to my screen, I have a short interrupt which causes a laggy/jerky behavior. In my case, I have a slide to unlock button, and on my device when the dispatch is call, the touch operation is interrupted, and become not intuitive and annoying. It's difficult to explain the problem, but my question is simple, how I have to set react-redux not to be laggy, or not interrupt current user interaction on redux dispatch ?
My app, is based on this project structure (for react-redux with Ble) : https://github.com/momolarson/BLEServiceDiscovery
Environement:
react-native: 0.63.3
react-native-ble-plx: 2.0.2
react-redux: 7.2.1
This is pseudo code of my app (the code is more longer, but I have excluded all other by remove them) :
HomeScreen.js
import stuff[...]
class HomeScreen extends Component {
componentDidMount() {
this.timer = setInterval(() => {
this.props.readTemp();
}, 2000);
}
render() {
const { value } = this.state
return (
<>
<ScrollView>
<Text>{this.props.temperatture}"></Text>
<Slide2Unlock/>
</ScrollView>
</>
);
}
}
function mapStateToProps(state) {
return {
temperature: state.temperature,
};
}
const mapDispatchToProps = dispatch => ({
readTemp: () => bluetooth.readTemp(),
})
export default connect(mapStateToProps, mapDispatchToProps())(HomeScreen);
redux's action file : actionBt.js (my file is based on this https://github.com/momolarson/BLEServiceDiscovery/blob/master/actions/index.js)
[...]
device.monitorCharacteristicForService(
characteristicData.serviceUUID,
characteristicData.uuid,
(error, characteristic) => {
if (characteristic != null && characteristic.value != null) {
dispatch(formatTemperature(characteristic.value));
}
},
);
thanks for your help
Update 1
I make a specific version of my app, without bluetooth, just the slide to unlock module and a watcher with setInterval, and still have a laggy behavior, when the state is dispatched. I have done tests with button only, when I tap then show the value via dispatch, it's still the same trouble.
this my test code, index.js (redux action file)
export const readTemp = () => {
return (dispatch, getState, DeviceManager) => {
const state = getState();
console.log("READ TEMP");
dispatch(temperatureSensor( Math.random(0,9) ))
}
}
function BLEservices(BLEServices) {
setInterval(() => {
BLEServices.readTemp();
}, 2500);
return (
<SafeAreaView style={styles.container}>
<Slider
childrenContainer={{ }}
onEndReached={() => {
console.log('REACHED')
}}
containerStyle={{
height:40,
margin: 8,
backgroundColor: "#EEEEEE",
overflow: 'hidden',
alignItems: 'center',
justifyContent: 'center',
width: '50%',
}}
sliderElement={
<Text style={{color:"#FFF"}}>TEST</Text>
}
>
<Text style={{color: "#D5BD9E"}}>unlock</Text>
</Slider>
<Text>Temperature: {BLEServices.temperatureSensor}</Text>
</SafeAreaView>
);
}
thanks for your advice, and your help
Update 2
Solution found, see my answer below. The problem was type of var user in dispatch and some side effect due to previous test I have done on app and not clean them.
I solved my problem, by finding multiple var who are contains objects. I have a var which contain four attributes, I update and use one of them. And this object was update by my watcher. When I dispatch object to get a part of this object, I have to read the whole object, and this one is fully updated by my watchern which cause laggy render. So i have splitted that, to update only per var.
Another thing I've done, I split my interface elements in multi component, before, I has a lot of code in one screen, because I didn't need to reuse them elsewhere.

firestore document id will be undefined after I update array

I have this flatlist which receive data from firestore and send as props to projectsummery.js
const ProjectList =({projects})=> {
return(
<FlatList
data={projects}
renderItem={(project,index)=>{
return(
<ProjectSummery project={project} key={project.item.id}
//keyExtractor={(item, index) =>item.id}/>
)
} }
/>
)
}
Here I have a button which which sends document id which is something like this
{project.item.id} == CSmet3tRjpjDcJ437M78
ProjectSummery.js
const ProjectSummery =(props)=> {
const {project,auth}=props
return(
<>
<View >
<Text> {project.item.title} </Text>
<Text>likes { project.item.likes.length}</Text>
<Text>{project.item.id}</Text>//document id in in firestore
<View>
<Button title='like' onPress{()=>props.likesPosts(project.item.id)}/>
</View>
</View>
const mapDispatchToProps=(dispatch)=>{
return{
likePosts:(postId)=>dispatch(likePosts(postId))
}
}
When I try to update array in firebase the first time it work but the second time the document id will be undefined. I use React-Native. Thanks for help...
export const likePosts = (postId) => {
return (dispatch,getState,{getFirebase,getFirestore})=>{
const profile=getState().firebase.profile
const authId=getState().firebase.auth.uid
const firestore=getFirestore()
firestore.collection('projects').doc(postId).update({
//this postId will be be undefined in the 2nd time
likes:firestore.FieldValue.arrayUnion({
likedAt:new Date(),
likedBy:authId,
name: profile.firstName
})
})
}}
The fist update postId == CSmet3tRjpjDcJ437M78 in the 2nd time postId will be undefined
What's happening is that when you click a like button the first time, it's working as expected so it gets the proper postId and then continues with the process you have defined. However, when you try the 2nd time it fails to fetch the postId as it's already liked.
The idea is that you'll need to either define an if statement and specify what should happen if it's already clicked and it get's clicked again (possibly storing the postId somewhere the first time and using it from there), or make an initial check that returns a specific message to the user if it's already clicked.
The issue has nothing to do with Firestore itself but with the button and the states of liked/unliked.
Here is one nice interactive example on codepen.io of a proper way of building like buttons using react. React Like Button
HTML
<div id="example"></div>
CSS
.btn-primary {
background-color: #23aa4e;
border-color: #177d37;
}
#example {
margin: 3rem;
}
.customContainer {
border: 1px solid black;
}
JS
class LikeButton extends React.Component {
constructor() {
super();
this.state = {
liked: false
};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState({
liked: !this.state.liked
});
}
render() {
const text = this.state.liked ? 'liked' : 'haven\'t liked';
const label = this.state.liked ? 'Unlike' : 'Like'
return (
<div className="customContainer">
<button className="btn btn-primary" onClick={this.handleClick}>
{label}</button>
<p>
you {text} this. Click to toggle.
</p>
</div>
);
}
}
ReactDOM.render(
<LikeButton />,
document.getElementById('example')
)

React Native components seem to be sharing a state

I'm having an issue with React-native where I have a component TouchTimer which uses an AnimatedTimer component. This timer is supposed to start and stop when it is tapped, which it does, however all of the TouchTimer components I add to a page will start and stop whenever any of them are tapped, rather than only affecting the tapped component.
Below is a snippet of my component:
TouchTimer.tsx
export class TouchTimer extends React.Component<TouchTimerProps> {
state: {
...
paused: boolean,
}
constructor(props) {
super(props);
...
this.state = {
...
paused: true,
}
}
startStop() {
this.setState({paused: !this.state.paused});
}
render() {
const { time } = this.props;
return (
<TouchableHighlight onPress={() => this.startStop()}>
<View>
<AnimatedTimer
...
time={time}
pause={this.state.paused}
/>
<View style={styles.timeContainer}>
<Text style={styles.time}>{this.state.remaining}</Text>
</View>
</View>
</TouchableHighlight>
)
}
}
And here is a snippet of the screen containing these components:
Details.tsx
import { TouchTimer } from '../components/TouchTimer';
...
export class RecipeDetailsScreen extends React.Component<NavigationInjectedProps> {
...
{this.state.steps.map(step => (
<List.Item
key={step.id}
title={"Step " + step.index}
style={styles.step}
description={step.short_desc}
right={() => (step.time > 0 &&
<TouchTimer
time={step.time * 60000}
/>
)}
/>
)
}
I have tried wrapping the TouchTimer components in a View and changing the paused boolean to a prop, to no avail.
I have also tested to see if this issue appears when the components are not siblings, and when they are not produced as the result of a callback, and the issue still persists in both these cases.
If anybody has any advice or answers on how to make these timers independent I would very much appreciate it!
Curiously that component seems to be implemented with a global pauseFlag that applies to all component instances. See https://github.com/dalisalvador/react-native-animated-timer/blob/master/src/Components/AnimatedTimer.js#L34
So I don't think you're doing anything wrong here, this is a limitation of the library code that is coupling all instances of your timer to the same pauseFlag value.

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!