Unstated store based React Navigation causing warning - react-native

I'm using react-navigation and Unstated in my react native project.
I have a situation where I would like use:
this.props.navigation.navigate("App")
after successfully signing in.
Problem is I don't want it done directly from a function assigned to a submit button. I want to navigate based upon a global Unstated store.
However, it means that I would need to use a conditional INSIDE of the Subscribe wrapper. That is what leads to the dreaded Warning: Cannot update during an existing state transition (such as within 'render').
render() {
const { username, password } = this.state;
return (
<Subscribe to={[MainStore]}>
{({ auth: { state, testLogin } }) => {
if (state.isAuthenticated) {
this.props.navigation.navigate("App");
return null;
}
console.log("rendering AuthScreen");
return (
<View style={styles.container}>
<TextInput
label="Username"
onChangeText={this.setUsername}
value={username}
style={styles.input}
/>
<TextInput
label="Password"
onChangeText={this.setPassword}
value={password}
style={styles.input}
/>
{state.error && (
<Text style={styles.error}>{state.error.message}</Text>
)}
<Button
onPress={() => testLogin({ username, password })}
color="#000"
style={styles.button}
>
Sign in!
</Button>
</View>
);
}}
</Subscribe>
);
It works. But what's the correct way to do it?
I don't have access to MainStore outside of Subscribe and therefore outside of render.

I'm not sure about the react-navigation patterns but you could use a wrapper around this component which subscribes to 'MainStore' and pass it down to this component as a prop. That way you'll have access to 'MainStore' outside the render method.

I have since found a better solution.
I created an HOC that I call now on any Component, functional or not, that requires access to the store. That give me access to the store's state and functions all in props. This means, I am free to use the component as it was intended, hooks and all.
Here's what it looks like:
WithUnstated.js
import React, { PureComponent } from "react";
import { Subscribe } from "unstated";
import MainStore from "../store/Main";
const withUnstated = (
WrappedComponent,
Stores = [MainStore],
navigationOptions
) =>
class extends PureComponent {
static navigationOptions = navigationOptions;
render() {
return (
<Subscribe to={Stores}>
{(...stores) => {
const allStores = stores.reduce(
// { ...v } to force the WrappedComponent to rerender
(acc, v) => ({ ...acc, [v.displayName]: { ...v } }),
{}
);
return <WrappedComponent {...allStores} {...this.props} />;
}}
</Subscribe>
);
}
};
export default withUnstated;
Used like so in this Header example:
import React from "react";
import { Text, View } from "react-native";
import styles from "./styles";
import { states } from "../../services/data";
import withUnstated from "../../components/WithUnstated";
import MainStore from "../../store/Main";
const Header = ({
MainStore: {
state: { vehicle }
}
}) => (
<View style={styles.plateInfo}>
<Text style={styles.plateTop}>{vehicle.plate}</Text>
<Text style={styles.plateBottom}>{states[vehicle.state]}</Text>
</View>
);
export default withUnstated(Header, [MainStore]);
So now you don't need to create a million wrapper components for all the times you need your store available outside of your render function.
As, as an added goodie, the HOC accepts an array of stores making it completely plug and play. AND - it works with your navigationOptions!
Just remember to add displayName to your stores (ES-Lint prompts you to anyway).
This is what a simple store looks like:
import { Container } from "unstated";
class NotificationStore extends Container {
state = {
notifications: [],
showNotifications: false
};
displayName = "NotificationStore";
setState = payload => {
console.log("notification store payload: ", payload);
super.setState(payload);
};
setStateProps = payload => this.setState(payload);
}
export default NotificationStore;

Related

React Native GraphQL nested data array returning error

I have tried everything I can think of to solve this and am still stumped. I am using AWS AppSync GraphQL to store a dataset that I would like to call into a SectionList.
For the SectionList I am using a hardcoded id to call the data set through a GraphQL query. The SectionList displays correctly when I am using dummy data. It also displays the 1-to-1 relationships in the API correctly.
I already configured amplify to increase the statement depth and I can see the data in the Object.
Code for the SectionList
import React, { useState, useEffect } from 'react';
import { View, StyleSheet, Text, Image, ImageBackground, ScrollView, TouchableOpacity, SectionList, SafeAreaView } from 'react-native';
import Feather from 'react-native-vector-icons/Feather';
import AntDesign from 'react-native-vector-icons/AntDesign';
import { API, graphqlOperation } from 'aws-amplify';
import { getGame, listGameSections, listGames } from '../graphql/queries';
const Item = ({ title }) => (
<View>
<Text>
{title}
</Text>
</View>
);
const GameScreen = ({ navigation }) => {
const [game, setGame] = useState([]);
useEffect(() => {
const fetchGame = async () => {
const gameInfo = { id: '0e2cb273-b535-4cf7-ab16-198c44a4991c'};
if (!gameInfo) {
return;
}
try {
const response = await API.graphql(graphqlOperation(getGame, {id: gameInfo.id}))
setGame(response.data.getGame);
console.log(response);
} catch (e) {
}
};
fetchGame();
}, [])
return (
<SafeAreaView>
<View>
<Text>
{game.name}
</Text>
</View>
<SectionList
sections={game.sections.items}
keyExtractor={(item, index) => item + index}
renderItem={({ item }) => <Item title={item} />}
renderSectionHeader={({ section: { title } }) => (
<View>
<Text>{title}</Text>
</View>
)}>
</SafeAreaView>
)
};
export default GameScreen;
Log of the object.
I am attempting to display the getGame.sections.items array but am returning an error undefined is not an object. Cannot read property items of undefined.
Please help, I am so stumped now. When I call game.name earlier in the function it displays correctly, but game.sections.items throws an error in the SectionList that it is undefined.
Xadm, you pointed me in the right direction. I added this to my code:
const [game, setGame] = useState({});
const [gameSection, setGameSection] = useState([]);
and in my useEffect:
setGameSection(response.data.getGame.sections.items)
When calling the data, game.name wanted an object, while game.sections.items wanted an array for the SectionList. Adding 2 different functions for each initial states, one for the objects and one for the array, was able to fix the problem and render the data.

ReferenceError: Can't find variable: props

In React-Native, I am creating a functional component called ImageSelector, in which I am using expo-image-picker and using the image URI as a required field in a parent component using Formik. My simulator works and I am able to successfully pick a generic image and log the image URI in the console ('success: ' + result.uri) but here are the following errors:
I want to display the image in the image component below but the image does not display (it does not break). I get the following error Unhandled promise rejection: ReferenceError: Can't find variable: props which I suppose is referring to the parent form component but I do not know what to change to get this error to go away.
Parent Component
import { View, Text, Button } from 'react-native';
import { Formik } from 'formik';
import * as yup from 'yup';
import ImageSelector from '../shared/imagePicker';
const newPostSchema = yup.object({
image: yup.string()
.required(),
})
export default function CreatePost({navigation}) {
const setImageURI = (image) => {
props.setFieldValue('imageUri', image.uri)
}
return (
<View style={styles?.container}>
<Formik
initialValues={{
imageURI: null,
}}
validationSchema={newPostSchema}
onSubmit={(values, actions) => {
console.log(values);
navigation.navigate('ReviewPost', {
imageURI: values.imageURI,
});
}}
>
{props => (
<View>
<ImageSelector
image={props.values.imageURI}
onImagePicked={setImageURI}
/>
<Button onPress={props.handleSubmit} title='REVIEW' />
</View>
)}
</Formik>
</View>
)
}
*** Nested ImageSelector Component in another file ***
import React, {useState, useEffect} from 'react';
import {View, Button, Image, StyleSheet} from 'react-native';
import * as ImagePicker from 'expo-image-picker';
const ImageSelector = ({image, onImagePicked}) => {
const [selectedImage, setSelectedImage] = useState();
useEffect(() => {
if(image) {
console.log('useEffect: ' + image);
setSelectedImage({uri: image});
}
}, [image])
pickImageHandler = async () => {
let result = await ImagePicker.launchImageLibraryAsync({
title: 'Choose Image',
maxWidth: 800,
maxHeight: 600,
mediaTypes: ImagePicker.MediaTypeOptions.All,
allowsEditing: true,
aspect: [4, 3],
quality: 1
});
if (!result.cancelled) {
console.log('success: ' + result.uri);
onImagePicked({uri: result.uri});
console.log('a');
setSelectedImage({uri: result.uri});
console.log('b');
}
if (result.cancelled) {
console.log('result cancelled: ' + result.cancelled);
}
}
return (
<View style={styles.container}>
<View style={styles.imageContainer}>
<Image source={selectedImage} />
</View>
<View style={styles.button}>
<Button title='Pick Image' onPress={this.pickImageHandler} />
</View>
</View>
)
}
The following 4 lines do not execute (console logs are for testing to ensure they don't get called):
onImagePicked({uri: result.uri});*
console.log('a'); *
setSelectedImage({uri: result.uri});*
console.log('b');*
I need to get the props-related error to go away, set selectedImage to equal result.uri, and have the image display in the <Image /> component using selectedImage.uri as the image source.
Help?
The problem here is in the error message. Since you are creating a functional component called CreatePost, the typical syntax for passing props would be
export default function CreatePost(props) {
...
}
So your component can access the props that are passed down to it, such as setFieldValue, however, you have used the spread operator {navigation} instead of props, so you are already extracting all the props when you do that. Thus, the scope does not know of any props variable. So, for now, I would try changing the argument to this
export default function CreatePost(props) {
const { navigation } = props;
...
}
That way wherever else in the scope you have referenced props will still work and you will not lose access to the navigation property either, alternatively, you can simply change 'navigation.navigate' to 'props.navigation.navigate' also. So javascript is saying cant find variable props, because to it, this is just a simple vanilla javascript function, and it does not intuitively know of a variable called props, you have to explicitly call it that.
Also, I feel like there might still be issues in this part of the code
{props => (
<View>
<ImageSelector
image={props.values.imageURI}
onImagePicked={setImageURI}
/>
<Button onPress={props.handleSubmit} title='REVIEW' />
</View>
)}
So it would help if you could post the code where you are using your component, to see what props, such as setFieldValue, navigation etc.you are passing.
You can just rewrite that part as
<Formik
initialValues={{
imageURI: null,
}}
validationSchema={newPostSchema}
onSubmit={(values, actions) => {
console.log(values);
navigation.navigate('ReviewPost', {
imageURI: values.imageURI,
});
}}
>
<View>
<ImageSelector
image={props.values.imageURI}
onImagePicked={setImageURI}
/>
<Button onPress={props.handleSubmit} title='REVIEW' />
</View>
Without doing the {props => part as with the refactor now you already have access to props in the scope.

AWS amplify remember logged in user in React Native app

I just started exploring AWS amplify as a backend for my react native application. Being a true beginner on using the service, I want my app to remember the logged in user every time I refresh the emulator.
I know from AWS amplify documentation that I can use the Auth function currentAuthenticatedUser for this purpose, but I have no idea on how to implement a code that does this purpose.
My app looks like this:
App.js
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import AuthTabs from './components/AuthTabs';
import NavigationTab from './components/NavigationTab';
import Amplify, { Auth } from 'aws-amplify';
import AWSConfig from './aws-exports';
Amplify.configure(AWSConfig);
export default class App extends React.Component {
state = {
isAuthenticated: false
}
authenticate(isAuthenticated) {
this.setState({ isAuthenticated })
}
render() {
if (this.state.isAuthenticated) {
console.log('Hello', Auth.user.username)
return(
<View style={styles.container}>
<Text style={styles.textStyle}>
Hello {Auth.user.username}!
</Text>
<NavigationTab
screenProps={
{authenticate: this.authenticate.bind(this)}
}
/>
</View>
)
}
return (
<View style={styles.container}>
<AuthTabs
screenProps={
{authenticate: this.authenticate.bind(this)}
}
/>
</View>
)
}
}
Any help would be much appreciated.
i have used it like this:
currentUser = () => {
Auth.currentAuthenticatedUser()
.then(user => {
console.log("USER", user);
this.props.navigation.navigate("App");
})
.catch(err => {
console.log("ERROR", err);
});
};
so, one can call it on the constructor on app refresh, and if the user is authenticated go to the main screen, but if it's not, stay in the login screen. Cheers.
I also have come up with a similar solution. But instead of the constructor, I use the life cycle method componentDidMount() to call a method that I named loadApp().
import React from 'react'
import {
StyleSheet,
View,
ActivityIndicator,
} from 'react-native'
import Auth from '#aws-amplify/auth'
export default class AuthLoadingScreen extends React.Component {
state = {
userToken: null
}
async componentDidMount () {
await this.loadApp()
}
// Get the logged in users and remember them
loadApp = async () => {
await Auth.currentAuthenticatedUser()
.then(user => {
this.setState({userToken: user.signInUserSession.accessToken.jwtToken})
})
.catch(err => console.log(err))
this.props.navigation.navigate(this.state.userToken ? 'App' : 'Auth')
}
render() {
return (
<View style={styles.container}>
<ActivityIndicator size="large" color="#fff" />
</View>
)
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#aa73b7',
alignItems: 'center',
justifyContent: 'center',
},
})
loadApp() will try and get the user JWT Token by calling the AWS Amplify currentAuthenticatedUser() method. The obtained token is then stored in the component state.
I have used React navigation version 2 to navigate the user to either the App screen or the Auth stack screen depending on her status: logged in or not logged in.
Here is the way I handled this issue:
const currentUserInfo = await Auth.currentUserInfo()
if (currentUserInfo){
const data = await Auth.currentAuthenticatedUser()
dispatch({authTypes.FETCH_USER_DATA_SUCCESS, {payload: {user: data}}});
}

React Admin - Get current value in a form

I am having big troubles getting the "updated" value of a record in an edit form. I always get the initial record values, even though I have an input linked to the right record source, which should update it.
Is there an alternative way to get the values of the SimpleForm ?
I have a simple edit form :
<Edit {...props}>
<SimpleForm>
<MyEditForm {...props} />
</SimpleForm>
</Edit>
MyEditForm is as follow:
class MyEditForm extends React.Component {
componentDidUpdate(prevProps, prevState, snapshot) {
console.log(prevProps.record.surface, this.props.record.surface); // <-- here is my problem, both values always get the initial value I had when I fetched the resource from API
}
render() {
return (
<div>
<TextInput source="surface" />
<!-- other fields -->
</div>
);
}
}
I usually do it this way to get my updated component's data from other components, but in the very case of a react-admin form, I can't get it to work.
Thanks,
Nicolas
It really depends on what you want to do with those values. If you want to hide/show/modify inputs based on the value of another input, the FormDataConsumer is the preferred method:
For example:
import { FormDataConsumer } from 'react-admin';
const OrderEdit = (props) => (
<Edit {...props}>
<SimpleForm>
<SelectInput source="country" choices={countries} />
<FormDataConsumer>
{({ formData, ...rest }) =>
<SelectInput
source="city"
choices={getCitiesFor(formData.country)}
{...rest}
/>
}
</FormDataConsumer>
</SimpleForm>
</Edit>
);
You can find more examples in the Input documentation. Take a look at the Linking Two Inputs and Hiding Inputs Based On Other Inputs.
However, if you want to use the form values in methods of your MyEditForm component, you should use the reduxForm selectors. This is safer as it will work even if we change the key where the reduxForm state is in our store.
import { connect } from 'react-redux';
import { getFormValues } from 'redux-form';
const mapStateToProps = state => ({
recordLiveValues: getFormValues('record-form')(state)
});
export default connect(mapStateToProps)(MyForm);
I found a working solution :
import { connect } from 'react-redux';
const mapStateToProps = state => ({
recordLiveValues: state.form['record-form'].values
});
export default connect(mapStateToProps)(MyForm);
When mapping the form state to my component's properties, I'm able to find my values using :
recordLiveValues.surface
If you don't want to use redux or you use other global state like me (recoil, etc.)
You can create custom-child component inside FormDataConsumer here example from me
// create FormReceiver component
const FormReceiver = ({ formData, getForm }) => {
useEffect(() => {
getForm(formData)
}, [formData])
return null
}
// inside any admin form
const AdminForm = () => {
const formState = useRef({}) // useRef for good performance not rerender
const getForm = (form) => {
formState.current = form
}
// you can access form by using `formState.current`
return (
<SimpleForm>
<FormDataConsumer>
{({ formData, ...rest }) => (
<FormReceiver formData={formData} getForm={getForm} />
)}
</FormDataConsumer>
</SimpleForm>
)
}

React Native Redux store dispatches reducers correctly, but doesn't update UI component

Working on a cancer treatment app in react native:
Current functionality: when I move the sliders and change the date on my app, it dispatches changes to the redux store successfully. Unfortunately, my UI doesn't update, even though I am calling the same store from the presentational components that I called for dispatch.
That results in this:
GIF of redux store changing while UI is static
Printing via
store.getState();
store.subscribe(() =>
console.log(store.getState())
);
I tried using subscription, but it seems like this isn't the right way to go about this. Thoughts?
snippets from my code (all in one small file, linked below)
Action
//action
function set_num_treatments(num) {
return {
type: SET_NUM_TREATMENTS,
num: num
}
}
setting the title
SET_NUM_TREATMENTS = "SET_NUM_TREATMENTS"
main reducer
function main_reducer(state = initialState, action) {
switch (action.type) {
case SET_PAGE_VIEW:
return Object.assign({}, state, {
current_page: action.page_of_interest
})
case SET_NUM_TREATMENTS:
return Object.assign({}, state, {
num_treatments: action.num
})
case SET_INTER_TREATMENT_INTERVAL:
return Object.assign({}, state, {
inter_treatment_interval: action.weeks_between_treatments
})
case SET_TREATMENT_START_DATE:
return Object.assign({}, state, {
treatment_start_date: action.date
})
default:
return state
}
return state
}
Here's where I start the store & produce the printing functionality
let store = createStore(main_reducer);
store.getState();
store.subscribe(() =>
console.log(store.getState())
);
here's the presentational components
class TreatmentSettings extends React.Component {
constructor(props){
super(props)
}
render() {
const props = this.props
const {store} = props
const state = store.getState()
return(
<View style={styles.treatment_option_slider_card}>
<Text style={styles.my_font, styles.tx_settings_header}>{state.num_treatments} Treatments</Text>
<Slider step={1} minimumValue={1} maximumValue={20} value={12}
onValueChange={(num_treatments) => {store.dispatch(set_num_treatments(num_treatments))}} />
<Text style={styles.my_font, styles.tx_settings_header}>X Weeks Between Treatments</Text>
<Slider step={1} minimumValue={1} maximumValue={4} value={2} style={{marginBottom:60}}
onValueChange={(value) => {store.dispatch(set_inter_treatment_interval(value))}}
/>
</View>
)
}
}
These final two components hold the main containers for the app
export default class App extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<Provider store={createStore(main_reducer)}>
<AppContainer />
</Provider>
);
}
}
class AppContainer extends React.Component {
constructor(props){
super(props)
}
render(){
return(
<View style={styles.container}>
<TreatmentSettings store={store} />
<Text>footertext</Text>
</View>
)
}
}
the one gist file is here if you want to see it all: https://github.com/briancohn/learning-redux/blob/navigation_addn/App.js
I really appreciate the help—
Thanks in advance!
-Brian
I think the way you are updating the store is fine but there’s something wrong with how your components are listening to the changes.
It seems you meant to use connect from react-redux for the containers to connect to the store. Then you can use mapStateToProps to get the data from the store to pass into the components as props. Check https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options for example.