Components not reacting to nested mobx stores - mobx

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 = () => {

Related

Mobx React `autorun` called more times on every change

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.

Cannot update a component (`Back`) while rendering a different component

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>
);
}

react-native-material-textfield Doesn't work as a controlled input

I'm using "react-native-material-textfield" for text inputs. I have a View to edit user details it fetch values from api when mounting and set it to state. But after upgrading "react-native-material-textfield" to "0.16.1" that original first name value is not shown in the text input after mounting. What I'm doing wrong here ?
constructor(props) {
super(props);
this.state = {
firstName: '',
};
}
componentDidMount(props) {
APIcall().then(data)=>{
this.setState({
firstName: data.firstName
});
}
}
<TextField
label="First Name"
value={this.state.firstName}
onChangeText={firstName => this.setState({firstName})}
/>
I ran into this after upgrading. In version 0.13.0 of the library, it switched to being a fully uncontrolled component according to the release notes.
Changed
defaultValue prop becomes current value on focus
value prop provides only initial value
Based on the current usage docs, there is now a method exposed for setting & getting the value using a ref to the component:
let { current: field } = this.fieldRef;
console.log(field.value());
(Personally, while I can maybe understand this improving performance because typing can often be fast for state updates, I'm not a fan of uncontrolled components since I want my state to drive the UI. I feel like this makes other live updates for validation very fiddly.)
In react-native-material-textfield, 'value' prop acts as default. To update the value you need to use ref. Get the ref using React.createRef(), then use setValue function
import React, { Component } from 'react';
import { TextField } from 'react-native-material-textfield';
import { View, Button } from 'react-native';
export default class TestComponent extends Component {
textField = React.createRef<TextField>();
constructor(props) {
super(props);
this.state = {
value: 'check',
};
}
onChangeText = () => {
// Send request via API to save the value in DB
};
updateText = () => {
if (this.textField && this.textField.current) {
this.textField.current.setValue('test');
}
};
render() {
return (
<View>
<TextField
label="Test value"
value={this.state.value}
onChangeText={this.onChangeText}
ref={this.textField}
/>
<Button onPress={this.updateText} />
</View>
);
}
}
Touch area in TextView
https://github.com/n4kz/react-native-material-textfield/issues/248
react-native-material-textfield
labelTextStyle={{ position: 'absolute', left: '100%' }}
label: {
fontFamily: fonts.Muli_SemiBold,
fontSize: 14,
letterSpacing: 0.1,
color: colors.gray90,
position: 'absolute', left: '100%'
},
<TextField
style={style.textInputRight}
labelTextStyle={style.label}
labelFontSize={16}}
onChangeText={value => onTextChange(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!

React Native: Updating state in onLayout gives "Warning: setState(...): Cannot update during an existing state transition"

I have a component in React Native which updates it's state once it knows what size it is.
Example:
class MyComponent extends Component {
...
render() {
...
return (
<View onLayout={this.onLayout.bind(this)}>
<Image source={this.state.imageSource} />
</View>
);
}
onLayout(event) {
...
this.setState({
imageSource: newImageSource
});
}
...
}
This gives the following error:
Warning: setState(...): Cannot update during an existing state transition (such as within render or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to componentWillMount.
I guess the onLayout function is called while still rendering (which can be good, the sooner the update, the better). What is the correct way to solve this problem?
Thanks in advance!
We got around this by using the measure function, you will have to wait until the scene is fully complete before measuring to prevent incorrect values (i.e. in componentDidMount/componentDidUpdate). Here's an example:
measureComponent = () => {
if (this.refs.exampleRef) {
this.refs.exampleRef.measure(this._logLargestSize);
}
}
_logLargestSize = (ox, oy, width, height, px, py) => {
if (height > this.state.measureState) {
this.setState({measureState:height});
}
}
render() {
return (
<View ref = 'exampleRef' style = {{minHeight: this.props.minFeedbackSize}}/>
);
}
Here is a solution from documentation for such cases
class MyComponent extends Component {
...
render() {
...
return (
<View>
<Image ref="image" source={this.state.imageSource} />
</View>
);
}
componentDidMount() {
//Now you can get your component from this.refs.image
}
...
}
But for my opinion it's better to do such things onload