MobX observable changes not updating components - react-native

I have a store in MobX that handles containing the cart, and adding items to it. However, when that data is updated the components don't re-render to suit the changes.
I tried using useEffect to set the total price, however when the cartData changes, the state is not updated. It does work on mount, though.
In another component, I call addItemToCart, and the console.warn function returns the proper data - but that data doesn't seem to be synced to the component. clearCart also seems to not work.
My stores are contained in one RootStore.
import React, {useState, useEffect} from 'react';
import {List, Text} from '#ui-kitten/components';
import {CartItem} from '_molecules/CartItem';
import {inject, observer} from 'mobx-react';
const CartList = (props) => {
const {cartData} = props.store.cartStore;
const [totalPrice, setTotalPrice] = useState(0);
const renderCartItem = ({item, index}) => (
<CartItem itemData={item} key={index} />
);
useEffect(() => {
setTotalPrice(cartData.reduce((a, v) => a + v.price * v.quantity, 0));
console.warn('cart changed!');
}, [cartData]);
return (
<>
<List
data={cartData}
renderItem={renderCartItem}
style={{backgroundColor: null, width: '100%'}}
/>
<Text style={{textAlign: 'right'}} category={'s1'}>
{`Total $${totalPrice}`}
</Text>
</>
);
};
export default inject('store')(observer(CartList));
import {decorate, observable, action} from 'mobx';
class CartStore {
cartData = [];
addItemToCart = (itemData) => {
this.cartData.push({
title: itemData.title,
price: itemData.price,
quantity: 1,
id: itemData.id,
});
console.warn(this.cartData);
};
clearCart = () => {
this.cartData = [];
};
}
decorate(CartStore, {
cartData: observable,
addItemToCart: action,
clearCart: action,
});
export default new CartStore();

I have solved the issue by replacing my addItemToCart action in my CartStore with this:
addItemToCart = (itemData) => {
this.cartData = [
...this.cartData,
{
title: itemData.title,
price: itemData.price,
quantity: 1,
id: itemData.id,
},
];
console.warn(this.cartData);
};

Related

react-native-wheel-selector how to set value and animate programmatically

using this package for wheel selector, and works fine, I just need to select value programmatically with animation(as happen user did).
Here is source code, which works cool, just need prop to select programmatically.
import React, { useRef, useMemo, useEffect, useState } from "react";
import { LinearGradient } from "expo-linear-gradient";
import WheelPickerExpo from "react-native-wheel-picker-expo";
const CITIES = "Jakarta,Bandung,Sumbawa,Taliwang,Lombok,Bima".split(",");
const ActionSheet = () => {
const [selectedRate, setSelectedRate] = useState(0);
useEffect(() => {
setTimeout(() => {
setSelectedRate(2);//changing initial select doesnt animate.
}, 4000);
}, []);
return (
<WheelPickerExpo
height={300}
width="100%"
initialSelectedIndex={selectedRate}
items={CITIES.map(name => ({ label: name, value: "" }))}
onChange={({ item }) => {
console.log(`item`, item);
}}
/>
);
};
export default ActionSheet;

Testing react-native app with jest. Problem in accessing context and Provider

I know the title is very vague but I hope someone may have an idea.
I want to perform a simple snapshot test on one of my screens with jest but I keep getting errors like this:
Warning: React.jsx: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.
14 | test('renders correctly', async () => {
15 | const tree = renderer.create(
> 16 | <AuthContext.AuthProvider>
| ^
17 | <AuthContext.Consumer>
18 | <ValidateScreenPhrase ref={(navigator)=>{ setNavigator(navigator) }}/>
19 | </AuthContext.Consumer>
The problem is probably that I use a Context build that looks as follows:
import React, { useReducer } from 'react'
export default (reducer, actions, defaultValue) => {
const Context = React.createContext();
const Provider = ({children}) => {
const [state, dispatch] = useReducer(reducer, defaultValue)
const boundActions = {}
for (let key in actions){
boundActions[key] = actions[key](dispatch);
}
return (
<Context.Provider value={{ state, ...boundActions }}>{children}</Context.Provider>
)
}
return { Context, Provider }
}
from here I then build different Contexts that contain functions and states as e.g.:
import createDataContext from "./createDataContext"
import { navigate } from '../navigationRef'
const authReducer = (state, action) => {
switch (action.type){
case 'clear_error_message':
return { ...state, errorMessage: '' }
default:
return state
}
}
const validateInput = (dispatch) => {
return (userInput, expected) => {
if (userInput === expected) {
navigate('done')
}
else{dispatch({ type: 'error_message', payload: 'your seed phrase was not typed correctly'})}
}
}
export const { Provider, Context } = createDataContext(
authReducer,
{ clearErrorMessage },
{ errorMessage: '' }
)
Now the screen that I want to test is this:
import React, { useState, useContext, useEffect } from 'react'
import { StyleSheet, View, TextInput, SafeAreaView } from 'react-native'
import { Text, Button } from 'react-native-elements'
import { NavigationEvents } from 'react-navigation'
import { Context as AuthContext } from '../context/AuthContext'
import BackButton from '../components/BackButton'
const ValidateSeedPhraseScreen = ({navigation}) => {
const { validateInput, clearErrorMessage, state } = useContext(AuthContext)
const [seedPhrase, setSeedPhrase] = useState('')
const testPhrase = 'blouse'
const checkSeedPhrase = () => {
validateInput(seedPhrase, testPhrase)}
return (
<SafeAreaView style={styles.container}>
<NavigationEvents
onWillFocus={clearErrorMessage}
/>
<NavigationEvents />
<BackButton routeName='walletInformation'/>
<View style={styles.seedPhraseContainer}>
<Text h3>Validate Your Seed Phrase</Text>
<TextInput
style={styles.input}
editable
multiline
onChangeText={(text) => setSeedPhrase(text)}
value={seedPhrase}
placeholder="Your Validation Seed Phrase"
autoCorrect={false}
autoCapitalize='none'
maxLength={200}
/>
<Button
title="Validate"
onPress={() => checkSeedPhrase(seedPhrase, testPhrase)}
style={styles.validateButton}
/>
{state.errorMessage ? (<Text style={styles.errorMessage}>{state.errorMessage}</Text> ) : null}
</View>
</SafeAreaView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
marginLeft: 25,
marginRight: 25
},
seedPhraseContainer:{
marginTop: '40%'
},
input: {
height: 200,
margin: 12,
borderWidth: 1,
padding: 10,
fontSize: 20,
borderRadius: 10
},
validateButton:{
paddingBottom: 15
}
})
export default ValidateSeedPhraseScreen
Here I import the AuthContext and make use of the function validateInput and state from the Context. Here I also don't know how to bring these into the testing file
and my test so far looks like this:
import React, {useContext} from "react";
import renderer from 'react-test-renderer';
import { setNavigator } from '../../src/navigationRef';
import ValidateScreenPhrase from '../../src/screens/ValidateSeedPhraseScreen'
import { Provider as AuthProvider, Context as AuthContext } from '../../src/context/AuthContext';
jest.mock('react-navigation', () => ({
withNavigation: ValidateScreenPhrase => props => (
<ValidateScreenPhrase navigation={{ navigate: jest.fn() }} {...props} />
), NavigationEvents: 'mockNavigationEvents'
}));
test('renders correctly', async () => {
const tree = renderer.create(
<AuthProvider>
<AuthContext.Consumer>
<ValidateScreenPhrase ref={(navigator)=>{ setNavigator(navigator) }}/>
</AuthContext.Consumer>
</AuthProvider>, {}).toJSON();
expect(tree).toMatchSnapshot();
});
I already tried out all lot of changes with the context and provider structure. I then always get errors like: "Authcontext is undefined" or "render is not a function".
Does anyone have an idea about how to approach this?

React Admin: how to pass state to transform

I have a component for creating media which uploads the media first to S3, then puts the returned values into the component's state:
import { Create, ReferenceInput, SelectInput, SimpleForm, TextInput } from 'react-admin';
import { Field } from 'react-final-form';
import React, { useState } from 'react';
import { ImageHandler } from './ImageHandler';
import { BeatLoader } from 'react-spinners';
export const MediaCreate = props => {
const [image, setImage] = useState(null);
const [isUploading, setIsUploading] = useState(false);
console.log(image) // <-- this contains the image object after uploading
const transform = data => {
console.log(image) // <-- this is NULL after clicking submit
return {
...data,
key: image.key,
mime_type: image.mime
}
};
return (
<Create {...props} transform={transform}>
<SimpleForm>
<SelectInput source="collection" label="Type" choices={[
{ id: 'gallery', name: 'Gallery' },
{ id: 'attachment', name: 'Attachment' },
]}/>
<ReferenceInput label="Asset" source="asset_id" reference="assets">
<SelectInput optionText="name"/>
</ReferenceInput>
<TextInput source={'name'} />
{isUploading &&
<div style={{ display: 'flex', alignItems: 'center' }}>
<BeatLoader
size={10}
color={"#123abc"}
loading={isUploading}
/> Uploading, please wait
</div>
}
<ImageHandler
isUploading={(isUploading) => setIsUploading(isUploading)}
onUploaded={(image) => setImage(image)}
/>
</SimpleForm>
</Create>
);
};
Why is image null despite containing the value after upload? How can I pass in my component state to the transform function?
This can be reached by useRef
export const MediaCreate = props => {
const [image, setImage] = useState(null);
const [isUploading, setIsUploading] = useState(false);
const someRef = useRef()
someRef.current = image
const transform = data => {
console.log(someRef.current) // <-- this will not be NULL
return {
...data,
key: someRef.current.key,
mime_type: someRef.current.mime
}
};

What is the best practice for unit testing my functional component with hooks in React Native?

I've written a simple wrapper component around ScrollView that enables/disables scrolling based on the available height it's given:
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {Keyboard, ScrollView} from 'react-native';
import {deviceHeight} from '../../../platform';
export default function ScrollingForm({
availableHeight = deviceHeight,
children,
}) {
const [scrollEnabled, setScrollEnabled] = useState(true);
const [formHeight, setFormHeight] = useState(0);
const scrollingForm = useRef(null);
const checkScrollViewEnabled = useCallback(() => {
setScrollEnabled(formHeight > availableHeight);
}, [availableHeight, formHeight]);
const onFormLayout = async event => {
await setFormHeight(event.nativeEvent.layout.height);
checkScrollViewEnabled();
};
useEffect(() => {
Keyboard.addListener('keyboardDidHide', checkScrollViewEnabled);
// cleanup function
return () => {
Keyboard.removeListener('keyboardDidHide', checkScrollViewEnabled);
};
}, [checkScrollViewEnabled]);
return (
<ScrollView
ref={scrollingForm}
testID="scrollingForm"
keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="handled"
scrollEnabled={scrollEnabled}
onLayout={onFormLayout}
onKeyboardDidShow={() => setScrollEnabled(true)}>
{children}
</ScrollView>
);
}
I need to write unit tests for this component. So far I have:
import React from 'react';
import {Keyboard} from 'react-native';
import {render} from 'react-native-testing-library';
import {Text} from '../..';
import ScrollingForm from './ScrollingForm';
describe('ScrollingForm', () => {
Keyboard.addListener = jest.fn();
Keyboard.removeListener = jest.fn();
let renderer;
describe('keyboard listener', () => {
it('renders text with default props', () => {
renderer = render(
<ScrollingForm>
<Text>Hello World!</Text>
</ScrollingForm>,
);
expect(Keyboard.addListener).toHaveBeenCalled();
});
it('should call listener.remove on unmount', () => {
renderer = render(
<ScrollingForm>
<Text>Goodbye World!</Text>
</ScrollingForm>,
);
renderer.unmount();
expect(Keyboard.removeListener).toHaveBeenCalled();
});
});
});
I want to also confirm that on layout if the available height is greater than the form height, that scrollEnabled is correctly being set to false. I've tried something like this:
it('sets scrollEnabled to false onFormLayout width 1000 height', () => {
mockHeight = 1000;
const {getByTestId} = render(
<ScrollingForm availableHeight={500}>
<Text>Hello World!</Text>
</ScrollingForm>,
);
const scrollingForm = getByTestId('scrollingForm');
fireEvent(scrollingForm, 'onLayout', {
nativeEvent: {layout: {height: mockHeight}},
});
expect(scrollingForm.props.scrollEnabled).toBe(false);
});

React-Native: pressing the button twice only updates the this.setState

The App is simple.. Conversion of gas.. What im trying to do is multiply the Inputed Amount by 2 as if it is the formula. So i have a index.js which is the Parent, and the Calculate.component.js who do all the calculations. What i want is, index.js will pass a inputed value to the component and do the calculations and pass it again to the index.js to display the calculated amount.
Index.js
import React, { Component } from 'react';
import { Text } from 'react-native';
import styled from 'styled-components';
import PickerComponent from './Picker.Component';
import CalculateAVGAS from './Calculate.component';
export default class PickerAVGAS extends Component {
static navigationOptions = ({ navigation }) => ({
title: navigation.getParam('headerTitle'),
headerStyle: {
borderBottomColor: 'white',
},
});
state = {
gasTypeFrom: 'Gas Type',
gasTypeTo: 'Gas Type',
input_amount: '',
pickerFrom: false,
pickerTo: false,
isResult: false,
result: '',
};
inputAmount = amount => {
this.setState({ input_amount: amount });
console.log(amount);
};
onResult = value => {
this.setState({
result: value,
});
console.log('callback ', value);
};
render() {
return (
<Container>
<Input
placeholder="Amount"
multiline
keyboardType="numeric"
onChangeText={amount => this.inputAmount(amount)}
/>
<ResultContainer>
<ResultText>{this.state.result}</ResultText>
</ResultContainer>
<CalculateContainer>
<CalculateAVGAS
title="Convert"
amount={this.state.input_amount}
total="total"
titleFrom={this.state.gasTypeFrom}
titleTo={this.state.gasTypeTo}
// isResult={this.toggleResult}
result={value => this.onResult(value)}
/>
</CalculateContainer>
</Container>
);
}
}
CalculateAVGAS / component
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
export default class CalculateAVGAS extends Component {
static propTypes = {
amount: PropTypes.string.isRequired,
titleFrom: PropTypes.string.isRequired,
titleTo: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
};
state = {
totalVisible: true,
result: '',
};
onPressConversion = () => {
const formula = this.props.amount * 2;
const i = this.props.result(this.state.result);
this.setState({ result: formula });
// console.log(this.state.result);
console.log('func()');
}
render() {
return (
<ButtonContainer onPress={() => this.onPressConversion()}>
<ButtonText>{this.props.title}</ButtonText>
</ButtonContainer>
);
}
}
After doing this, the setState only updates when pressing the Convert button twice
your issue here is that you want to display information in the parent component, but you are saving that info in the child component's state.
Just pass amount, and result, to the stateless child component (CalculateAVGAS).
It's usually best to keep child components "dumb" (i.e. presentational) and just pass the information they need to display, as well as any functions that need to be executed, as props.
import React, {Component} from 'react';
import styled from 'styled-components';
export default class CalculateAVGAS extends Component {
onPressConversion = () => {
this.props.result(this.props.amount * 2);
};
render() {
return (
<ButtonContainer onPress={() => this.onPressConversion()}>
<ButtonText>{this.props.title}</ButtonText>
</ButtonContainer>
);
}
}
const ButtonContainer = styled.TouchableOpacity``;
const ButtonText = styled.Text``;
Parent component looks like:
import React, {Component} from 'react';
import {Text} from 'react-native';
import styled from 'styled-components';
import CalculateAVGAS from './Stack';
export default class PickerAVGAS extends Component {
static navigationOptions = ({navigation}) => ({
title: navigation.getParam('headerTitle'),
headerStyle: {
borderBottomColor: 'white',
},
});
state = {
gasTypeFrom: 'Gas Type',
gasTypeTo: 'Gas Type',
input_amount: null,
pickerFrom: false,
pickerTo: false,
isResult: false,
result: null,
};
inputAmount = amount => {
this.setState({input_amount: amount});
};
onResult = value => {
this.setState({
result: value,
});
};
render() {
return (
<Container>
<Input
placeholder="Amount"
multiline
keyboardType="numeric"
onChangeText={amount => this.inputAmount(amount)}
/>
<ResultContainer>
<ResultText>{this.state.result}</ResultText>
</ResultContainer>
<CalculateContainer>
<CalculateAVGAS
title="Convert"
amount={this.state.input_amount}
total="total"
titleFrom={this.state.gasTypeFrom}
titleTo={this.state.gasTypeTo}
result={value => this.onResult(value)}
/>
</CalculateContainer>
</Container>
);
}
}
const Container = styled.View``;
const ResultContainer = styled.View``;
const ResultText = styled.Text``;
const CalculateContainer = styled.View``;
const Input = styled.TextInput``;