React Native: How to test a async component snapshot with Jest/Testing-library? - react-native

I am trying to create a test for an async component in React Native. This component uses useEffect to fetch for data, sets it to a state variable and loads the screen accordingly. Once it is all loaded I'd like to compare it to a snapshot. The issue I am having is my test is synchronous, when I check the rendered snapshot it has my loading indicator.
How can I wait for it to load the data and then perform tests?
All the examples and tutorials I find are for sync components, involving simple tasks like checking a button for a specific title, this and the other. I've tried waitFor function but it times out before the data is fetched, apparently it has a 5 second limit. Or maybe I should mock a fetch (?) but my component doesn't take any props to inject the data into it.
To be honest I am very confused on how to approach this. I've never done any automated tests before.

After much confusion in my head I figured it out.
In Jest, whenever you use an external source like an API call using fetch or axios, you have to mock it. This means that Jest will take any axios requests from your component or function and instead of calling the real axios it will call your mock axios automatically. This was the explanation that I was missing and the source of my confusion. The beauty of jest mocking is that you will always get the same data for your tests keeping results and assertions consistent.
There are many ways to mock Axios with Jest including libraries for this specific purpose like jest-mock-axios and MSW (Mock Service Worker) but I couldn't get them to work in my case.
I found a much easier way without the need of external libraries described in the following YouTube tutorial. This guy knows how to explain things and he has a newer video using MSW (link in the YouTube comments).
YouTube: Mocking Axios in Jest + Testing Async Functions
Solution
This is the component to be tested, as you can see there is a axios request triggered by useEffect on mount.
/screens/Home.tsx
import React, { useState, useEffect, memo } from "react";
import { FlatList, StyleSheet } from "react-native";
import { Button } from "react-native-elements";
import axios from "axios";
import Item from "../components/Item";
import AppConfig from "../AppConfig.json";
import { View, Text, ActivityIndicator } from "../components/Themed";
import Toast from "react-native-toast-message";
import { RootTabScreenProps } from "../types";
let _isMounted = false;
function Home({ navigation }: RootTabScreenProps<"Shop">) {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState("");
useEffect(() => {
_isMounted = true;
loadData();
return () => {
_isMounted = false;
};
}, []);
async function loadData(cb?: any) {
try {
axios.get(`${AppConfig.api}/products`).then((res) => {
if (!_isMounted) return;
if (res.status === 200) {
const { data } = res;
setItems(data);
} else {
setError(`Error ${res.status}: failed to load products`);
}
setLoading(false);
setRefreshing(false);
if (typeof cb === "function") cb();
});
} catch (error) {
setLoading(false);
setRefreshing(false);
setError("Failed to load products");
// console.log(error);
}
}
if (loading && !error) {
return (
<View style={styles.containerCenter}>
<ActivityIndicator size={"large"} color="primary" />
</View>
);
} else if (!loading && error) {
return (
<View style={styles.containerCenter}>
<Text>{error}</Text>
<Button
title="Try again"
onPress={() => {
setLoading(true);
setError("");
loadData();
}}
/>
</View>
);
} else {
return (
<View style={styles.container}>
<FlatList
columnWrapperStyle={{ justifyContent: "space-between" }}
data={items}
numColumns={2}
renderItem={({ item }: any) => {
return (
<Item
item={item}
onPress={() => navigation.push("Product", item)}
/>
);
}}
keyExtractor={(item: object, index: any) => index}
refreshing={refreshing}
onRefresh={() => {
setRefreshing(true);
loadData(() => {
Toast.show({
type: "success",
text1: "Product list refreshed",
position: "bottom",
});
});
}}
/>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
containerCenter: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
title: {
fontSize: 20,
fontWeight: "bold",
},
separator: {
marginVertical: 30,
height: 1,
width: "80%",
},
textError: {
fontSize: 18,
marginBottom: 10,
maxWidth: 250,
},
});
export default memo(Home);
Step 1
Create a folder at the root of your project called __mocks__ (or wherever your source code is!) and create a file called axios.js containing the following object:
/__mocks__/axios.js
export default {
get: jest.fn(() =>
Promise.resolve({
headers: {},
config: {},
status: 200,
statusText: "OK",
data: [
{
id: 1,
title: "Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops",
price: 109.95,
description:
"Your perfect pack for everyday use and walks in the forest. Stash your laptop (up to 15 inches) in the padded sleeve, your everyday",
category: "men's clothing",
image: "https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg",
rating: { rate: 3.9, count: 120 },
},
{
id: 2,
title: "Mens Casual Premium Slim Fit T-Shirts ",
price: 22.3,
description:
"Slim-fitting style, contrast raglan long sleeve, three-button henley placket, light weight & soft fabric for breathable and comfortable wearing. And Solid stitched shirts with round neck made for durability and a great fit for casual fashion wear and diehard baseball fans. The Henley style round neckline includes a three-button placket.",
category: "men's clothing",
image:
"https://fakestoreapi.com/img/71-3HjGNDUL._AC_SY879._SX._UX._SY._UY_.jpg",
rating: { rate: 4.1, count: 259 },
},
{
id: 3,
title: "Mens Cotton Jacket",
price: 55.99,
description:
"great outerwear jackets for Spring/Autumn/Winter, suitable for many occasions, such as working, hiking, camping, mountain/rock climbing, cycling, traveling or other outdoors. Good gift choice for you or your family member. A warm hearted love to Father, husband or son in this thanksgiving or Christmas Day.",
category: "men's clothing",
image: "https://fakestoreapi.com/img/71li-ujtlUL._AC_UX679_.jpg",
rating: { rate: 4.7, count: 500 },
},
],
})
),
};
Adjust the response from your Promise.resolve(...) to whatever you expect your real API to return. Also, make sure you are mocking the function by using jest.fn() otherwise this will not work.
You can also add different properties to you mocked axios object like post, update, put or whatever type of request you need to mock.
Step 2
In the root of your source code again, create a folder called __tests__ and inside of it create a folder called screens. Then create your test file, to keep things consistent I named mine Home.test.js.
/__tests__/screens/Home.test.js
import renderer from "react-test-renderer";
import axios from "axios";
import { act } from "#testing-library/react-native";
import Home from "../../screens/Home";
// Important:
// By calling this, jest will know not to use the real axios and will load it
// from your __mocks__ folder.
jest.mock("axios");
describe("<Home />", () => {
let wrapper;
it("renders items", async () => {
await act(async () => {
// This is where the magic happens, when you render your Home component and useEffect
// goes to perform your axios request jest will automatically call your __mocks__/axios instead
wrapper = await renderer.create(<Home />);
});
await expect(wrapper.toJSON()).toMatchSnapshot();
});
it("renders error", async () => {
await act(async () => {
// You can also override your __mocks__/axios by doing the following and simulate a different
// response from your mocking axios
await axios.get.mockImplementationOnce(() =>
Promise.resolve({
status: 400,
statusText: "400",
headers: {},
config: {},
})
);
wrapper = await renderer.create(<Home />);
});
await expect(wrapper.toJSON()).toMatchSnapshot();
});
});
Now when you run npm run test your Home component will receive the data from your __mocks__/axios and render it as expected and you can perform all sorts of tests on it.
This is actually really cool!

Related

Warning: React has detected a change in the order of Hooks

I have run into this error in my code, and don't really know how to solve it, can anyone help me?
I get the following error message:
ERROR Warning: React has detected a change in the order of Hooks called by ScreenA. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks
import React, { useCallback, useEffect, useState } from "react";
import { View, Text, StyleSheet, Pressable } from "react-native";
import { useNavigation } from '#react-navigation/native';
import { DancingScript_400Regular } from "#expo-google-fonts/dancing-script";
import * as SplashScreen from 'expo-splash-screen';
import * as Font from 'expo-font';
export default function ScreenA({ route }) {
const [appIsReady, setAppIsReady] = useState(false);
useEffect(() => {
async function prepare() {
try {
// Keep the splash screen visible while we fetch resources
await SplashScreen.preventAutoHideAsync();
// Pre-load fonts, make any API calls you need to do here
await Font.loadAsync({ DancingScript_400Regular });
// Artificially delay for two seconds to simulate a slow loading
// experience. Please remove this if you copy and paste the code!
await new Promise(resolve => setTimeout(resolve, 2000));
} catch (e) {
console.warn(e);
} finally {
// Tell the application to render
setAppIsReady(true);
}
}
prepare();
}, []);
const onLayoutRootView = useCallback(async () => {
if (appIsReady) {
// This tells the splash screen to hide immediately! If we call this after
// `setAppIsReady`, then we may see a blank screen while the app is
// loading its initial state and rendering its first pixels. So instead,
// we hide the splash screen once we know the root view has already
// performed layout.
await SplashScreen.hideAsync();
}
}, [appIsReady]);
if (!appIsReady) {
return null;
}
const navigation = useNavigation();
const onPressHandler = () => {
// navigation.navigate('Screen_B', { itemName: 'Item from Screen A', itemID: 12 });
}
return (
<View style={styles.body} onLayout={onLayoutRootView}>
<Text style={styles.text}>
Screen A
</Text>
<Pressable
onPress={onPressHandler}
style={({ pressed }) => ({ backgroundColor: pressed ? '#ddd' : '#0f0' })}
>
<Text style={styles.text}>
Go To Screen B
</Text>
</Pressable>
<Text style={styles.text}>{route.params?.Message}</Text>
</View>
)
}
const styles = StyleSheet.create({
body: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
text: {
fontSize: 40,
margin: 10,
fontFamily: 'DancingScript_400Regular'
}
})
I have read the rules of hooks: https://reactjs.org/docs/hooks-rules.html
The output is correct, but i want to fix this error before i add more additions to the app
You need to move useNavigation use before early returns.
Instead, always use Hooks at the top level of your React function, before any early returns.
The key is you need to call all the hooks in the exact same order on every component lifecycle update, which means you can't use hooks with conditional operators or loop statements such as:
if (customValue) useHook();
// or
for (let i = 0; i< customValue; i++) useHook();
// or
if (customValue) return;
useHook();
So moving const navigation = useNavigation(); before if (!appIsReady) {return null;}, should solve your problem:
export default function ScreenA({ route }) {
const [appIsReady, setAppIsReady] = useState(false);
const navigation = useNavigation();
// ...
}

Issue sending & receiving streams between two clients in LiveKit's React Native SDK

I'm trying to build on the example app provided by livekit, so far I've implemented everything like the example app and I've been successful with connecting to a room on example website, I recieve audio from website, but I don't read the video stream, and I also can't send audio or video at all.
Steps to reproduce the behavior:
add the following to index.js
import { registerRootComponent } from "expo";
import { registerGlobals } from "livekit-react-native";
import App from "./App";
registerRootComponent(App);
registerGlobals();
Rendering the following component in App.tsx
import { Participant, Room, Track } from "livekit-client";
import {
useRoom,
useParticipant,
AudioSession,
VideoView,
} from "livekit-react-native";
import { useEffect, useState } from "react";
import { Text, ListRenderItem, StyleSheet, FlatList, View } from "react-native";
import { ParticipantView } from "./ParticipantView";
import { RoomControls } from "./RoomControls";
import type { TrackPublication } from "livekit-client";
const App = () => {
// Create a room state
const [, setIsConnected] = useState(false);
const [room] = useState(
() =>
new Room({
publishDefaults: { simulcast: false },
adaptiveStream: true,
})
);
// Get the participants from the room
const { participants } = useRoom(room);
const url = "[hard-coded-url]";
const token =
"[hard-coded-token";
useEffect(() => {
let connect = async () => {
// If you wish to configure audio, uncomment the following:
await AudioSession.configureAudio({
android: {
preferredOutputList: ["speaker"],
},
ios: {
defaultOutput: "speaker",
},
});
await AudioSession.startAudioSession();
await room.connect(url, token, {});
await room.localParticipant.setCameraEnabled(true);
await room.localParticipant.setMicrophoneEnabled(true);
await room.localParticipant.enableCameraAndMicrophone();
console.log("connected to ", url);
setIsConnected(true);
};
connect();
return () => {
room.disconnect();
AudioSession.stopAudioSession();
};
}, [url, token, room]);
// Setup views.
const stageView = participants.length > 0 && (
<ParticipantView participant={participants[0]} style={styles.stage} />
);
const renderParticipant: ListRenderItem<Participant> = ({ item }) => {
return (
<ParticipantView participant={item} style={styles.otherParticipantView} />
);
};
const otherParticipantsView = participants.length > 0 && (
<FlatList
data={participants}
renderItem={renderParticipant}
keyExtractor={(item) => item.sid}
horizontal={true}
style={styles.otherParticipantsList}
/>
);
const { cameraPublication, microphonePublication } = useParticipant(
room.localParticipant
);
return (
<View style={styles.container}>
{stageView}
{otherParticipantsView}
<RoomControls
micEnabled={isTrackEnabled(microphonePublication)}
setMicEnabled={(enabled: boolean) => {
room.localParticipant.setMicrophoneEnabled(enabled);
}}
cameraEnabled={isTrackEnabled(cameraPublication)}
setCameraEnabled={(enabled: boolean) => {
room.localParticipant.setCameraEnabled(enabled);
}}
onDisconnectClick={() => {
// navigation.pop();
console.log("disconnected");
}}
/>
</View>
);
};
function isTrackEnabled(pub?: TrackPublication): boolean {
return !(pub?.isMuted ?? true);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
justifyContent: "center",
},
stage: {
flex: 1,
width: "100%",
},
otherParticipantsList: {
width: "100%",
height: 150,
flexGrow: 0,
},
otherParticipantView: {
width: 150,
height: 150,
},
});
export default App;
the components used here are mostly the same as what's in the example, I've removed the screensharing logic and the messages
5. I run the app using an expo development build
6. it will log that it's connected, you'll be able to hear sound from the remote participant, but not see any video or send any sound.
7. if i try to add
await room.localParticipant.enableCameraAndMicrophone();
in the useEffect, I get the following error:
Possible Unhandled Promise Rejection (id: 0):
Error: Not implemented.
getSettings#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:103733:24
#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:120307:109
generatorResume#[native code]
asyncGeneratorStep#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:21908:26
_next#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:21927:29
#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:21932:14
tryCallTwo#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:26656:9
doResolve#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:26788:25
Promise#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:26675:14
#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:21924:25
#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:120173:52
generatorResume#[native code]
asyncGeneratorStep#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:21908:26
_next#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:21927:29
tryCallOne#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:26648:16
#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:26729:27
#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:27687:26
_callTimer#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:27602:17
_callReactNativeMicrotasksPass#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:27635:17
callReactNativeMicrotasks#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:27799:44
__callReactNativeMicrotasks#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:21006:46
#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:20806:45
__guard#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:20986:15
flushedQueue#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:20805:21
flushedQueue#[native code]
Expected behavior
This should both receive & send video and audio streams between the two clients

React: how to initialise redux state before rendering component?

I'm trying to create a basic app with a user login feature using Redux to manage the user details. I've linked a GIF of my screen below and, as you can see, there is a delay between loading the component and the user details rendering. My code for the component profile is also noted.
Name of user delay when loading
import React, {Component} from 'react';
import {View, StyleSheet, Text, TouchableOpacity} from 'react-native';
import {connect} from 'react-redux';
import {fetchProfile} from '../../actions/ProfileActions';
import {logoutUser} from '../../actions/AuthActions';
class Profile extends Component {
state = {
firstName: '',
lastName: '',
email: '',
goals: '',
};
componentDidMount() {
this.props.fetchProfile();
}
componentDidUpdate(prevProps) {
if (prevProps !== this.props) {
this.setState({
firstName: this.props.profile.firstName,
lastName: this.props.profile.lastName,
email: this.props.profile.email,
goals: this.props.profile.goals,
});
}
}
onPressLogout = () => {
this.props.logoutUser();
};
render() {
return (
<View style={styles.container}>
<View style={styles.headerContainer}>
<Text style={styles.header}>
Profile of {this.state.firstName} {this.state.lastName}
</Text>
</View>
<View style={styles.textContainer}>
<TouchableOpacity
style={styles.buttonContainer}
onPress={this.onPressLogout.bind(this)}>
<Text style={styles.buttonText}>Logout</Text>
</TouchableOpacity>
</View>
</View>
);
}
}
const mapStateToProps = (state) => ({
profile: state.profile.profile,
});
export default connect(mapStateToProps, {fetchProfile, logoutUser})(Profile);
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F9F9F9',
},
headerContainer: {
marginTop: 75,
marginLeft: 20,
},
header: {
fontSize: 34,
fontWeight: '700',
color: '#000000',
},
textContainer: {
flex: 1,
justifyContent: 'flex-end',
alignItems: 'center',
marginBottom: 30,
},
buttonContainer: {
backgroundColor: '#34495E',
alignItems: 'center',
padding: 12,
width: 350,
borderRadius: 15,
},
buttonText: {
color: 'white',
fontWeight: 'bold',
fontSize: 19,
},
});
EDIT: I forgot to explain what fetchProfile() does. It connects to the firebase database to retrieve the user's details. Code below:
import {PROFILE_FETCH} from './types';
import firebase from 'firebase';
export const fetchProfile = () => {
const {currentUser} = firebase.auth();
return (dispatch) => {
firebase
.database()
.ref(`/users/${currentUser.uid}/profile`)
.on('value', (snapshot) => {
dispatch({
type: PROFILE_FETCH,
payload: snapshot.val(),
});
});
};
};
Furthermore, I have 3 different screens in the app, all of which will probably make use of the user's details. I'm sure there must be a more efficient way of only having to fetchProfile() once and then passing the details to each component, somehow...
How can I have it so when the user logs in, their details are already loaded in the component, so there is no delay? Thanks in advance!
One way I've gotten around this is by conditionally rendering a skeleton if it is still loading and then actually rendering the details once finished.
I'm not sure if this is exactly the solution you're looking for (and you may already know it's an option), but maybe this helps?
Using firebase you must create a listener.
do something like this:
Reducer Action:
// Action Creators
export function onAuthChanged(fb) {
return async (dispatch) => {
fb.auth().onAuthStateChanged((res) => {
dispatch({
type: Types.SET_ATTR,
payload: {
attr: 'user',
value: res,
},
});
});
};
}
call this function from a FirebaseProvider componentWillMount function
then
put the FirebaseProvider on your App class;
const App = () => {
return (
<>
<Provider store={store}>
<TranslatorProvider />
<FirebaseProvider />
<ThemeProvider>
<TrackingProvider>
<Routes />
</TrackingProvider>
</ThemeProvider>
</Provider>
</>
);
};
the listener will save on your reducer the user when it login and logout
According to what you have provided, definitely there will be a delay. I'll explain what is happening here.
You are requesting data from the firebase after you have rendered the details on Profile. This happens because you are requesting data in componentDidMount method. This method gets called first time Render method is completely finished rendering your components. So I'll suggest you two methods to get rid of that.
As Coding Duck suggested, you can show a skeleton loader until you fetch data from the firebase.
You can request these data from your login. That means, if user authentication is success, you can request these data using fetchProfile action and once you fetch these data completely, you can use Navigation.navigate('Profile') to navigate to your Profile screen rather than directly navigate to it once the authentication is success. In that time since you have fetched data already, there will be no issue.
Also you can use firebase persist option to locally store these data. So even if there were no internet connection, still firebase will provide your profile information rapidly.
EDIT
More specific answer with some random class and function names. This is just an example.
Let's say onLogin function handles all your login requirements in your authentication class.
onLogin = () => {
/** Do the validation and let's assume validation is success */
/** Now you are not directly navigating to your Profile page like you did in the GIF. I assume that's what you did because you have not added more code samples to fully understand what you have done.*/
/** So now you are calling the fetchProfile action through props and retrieve your details. */
this.props.fetchProfile(this.props.navigation);
};
Now let's modify your fetchDetails action.
export const fetchProfile = (navigation) => {
const {currentUser} = firebase.auth();
return (dispatch) => {
firebase
.database()
.ref(`/users/${currentUser.uid}/profile`)
.on('value', (snapshot) => {
dispatch({
type: PROFILE_FETCH,
payload: snapshot.val(),
});
navigation.navigate('Profile')
});
};
};
Note : This is not the best method of handling navigations but use a global navigation service to access directly top level navigator. You can learn more about that in React Navigation Documentation. But let's use that for now in this example.
So as you can see, when user login is successful, now you are not requesting data after rendering the Profile page but request data even before navigating to the page. So this ensures that profile page is only getting loaded with relevant data and there will be no lag like in your GIF.

Use Native Base Toast To Show Error From Redux Action

I'm using NativeBase in a React Native app. I'm trying to show a Toast component based on an error that is set in an redux action because it happens via a call to the API.
It will show now, but currently I get the warning message:
Warning: 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'm not sure how I could bind this or what I could do to solve the warning.
Render Method
render() {
return (
<View>
{this.renderError()}
{this.renderForm()}
</View>
);
}
Render Error Method
renderError() {
if (this.props.error.type === 'server') {
return (
Toast.show({
text: this.props.error.message,
buttonText: 'Okay',
duration: 5000,
type: 'danger'
})
);
}
}
Versions
React Native: 0.55.4
Native Base: 2.4.5
Edit: Adding an example for clarity
I need to show a Toast based on the response from the server. For example, if the username and password don't match an account, I need to render the Toast.
Solution:
I ended up creating a ToastService:
import { Toast } from 'native-base';
function showToast(message) {
return (
Toast.show({
text: message,
buttonText: 'Okay',
duration: 5000,
type: 'danger'
})
);
}
export default {
showToast
};
and now in my action I can just call:
ToastService.showToast(data);
You can create a function and call this one outside. But make sure your app is wrap with the Root component of native-base. No need to return a component like you do. Calling this function gonna show the toastr and now you have the freedom to call from anywhere. But make sure Root component wrap your app.
import { Toast } from 'native-base';
export const toastr = {
showToast: (message, duration = 2500) => {
Toast.show({
text: message,
duration,
position: 'bottom',
textStyle: { textAlign: 'center' },
buttonText: 'Okay',
});
},
};
Now inside your action you can call the toastr function
toastr.showToast('Verication code send to your phone.');
Or in redux actions
const signup = values => dispatch => {
try {
// your logic here
} catch (error) {
toastr.showToast(error.message)
}
}
I solved this issue by using React Hooks.
() => {
useEffect(() => {
if(error) {
Toast.show({
text: this.props.error.message,
buttonText: 'Okay',
duration: 5000,
type: 'danger'
})
}
})
return (
<View>
{this.renderForm()}
</View>
);
}
Check React Native Seed for this implementation
https://reactnativeseed.com/
Like Dwayne says above, you need to use useEffect so that Toast is called before the render cycle. You can wrap this is a component like so:
const ErrorToast: React.FC = () => {
const {state} = useCollections();
useEffect(() => {
if(state.errored) {
Toast.show({
text: 'Oops. There has been an error',
duration: 2000
});
}
});
return null;
}
And then simply include it as <ErrorToast />

React Native: Possible unhandled promise rejection

I'm getting the following error:
Possible unhandled promise rejection (id:0: Network request failed)
Here's the promise code, I don't see what's wrong here, any ideas?
return fetch(url)
.then(function(response){
return response.json();
})
.then(function(json){
return {
city: json.name,
temperature: kelvinToF(json.main.temp),
description: _.capitalize(json.weather[0].description)
}
})
.catch(function(error) {
console.log('There has been a problem with your fetch operation: ' + error.message);
});
}
Edit:
I added a catch function and got a better error:
You passed an undefined or null state object; instead, use forceUpdate(). index.ios.js:64 undefined
Here's the index.ios.js code. The url is fine and giving me the correct json data. I can see with console log that both region.latitude and region.longitude are available in Api(region.latitude, region.longitude). But data is undefined.
I'm still not sure what's going on, why there's a problem with data and why it's undefined.
// var React = require('react-native'); --deprecated
// updated
import React from 'react';
// updated
import {
AppRegistry,
MapView,
View,
Text,
StyleSheet,
} from 'react-native';
/*
var {
AppRegistry,
MapView,
View,
Text,
StyleSheet
} = React;
*/ // -- depreciated
var Api = require('./src/api');
var Weather = React.createClass({
getInitialState: function() {
return {
pin: {
latitude: 0,
longitude: 0
},
city: '',
temperature: '',
description: ''
};
},
render: function() {
return <View style={styles.container}>
<MapView
annotations={[this.state.pin]}
onRegionChangeComplete={this.onRegionChangeComplete}
style={styles.map}>
</MapView>
<View style={styles.textWrapper}>
<Text style={styles.text}>{this.state.city}</Text>
<Text style={styles.text}>{this.state.temperature}</Text>
<Text style={styles.text}>{this.state.description}</Text>
</View>
</View>
},
onRegionChangeComplete: function(region) {
this.setState({
pin: {
longitude: region.longitude,
latitude: region.latitude
}
});
Api(region.latitude, region.longitude)
.then((data) => {
console.log(data);
this.setState(data);
});
}
});
var styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'stretch',
backgroundColor: '#F5FCFF'
},
map: {
flex: 2,
marginTop: 30
},
textWrapper: {
flex: 1,
alignItems: 'center'
},
text: {
fontSize: 30
}
});
AppRegistry.registerComponent('weather', () => Weather);
catch function in your api should either return some data which could be handled by Api call in React class or throw new error which should be caught using a catch function in your React class code. Latter approach should be something like:
return fetch(url)
.then(function(response){
return response.json();
})
.then(function(json){
return {
city: json.name,
temperature: kelvinToF(json.main.temp),
description: _.capitalize(json.weather[0].description)
}
})
.catch(function(error) {
console.log('There has been a problem with your fetch operation: ' + error.message);
// ADD THIS THROW error
throw error;
});
Then in your React Class:
Api(region.latitude, region.longitude)
.then((data) => {
console.log(data);
this.setState(data);
}).catch((error)=>{
console.log("Api call error");
alert(error.message);
});
You should add the catch() to the end of the Api call. When your code hits the catch() it doesn't return anything, so data is undefined when you try to use setState() on it. The error message actually tells you this too :)
According to this post, you should enable it in XCode.
Click on your project in the Project Navigator
Open the Info tab
Click on the down arrow left to the "App Transport Security Settings"
Right click on "App Transport Security Settings" and select Add Row
For created row set the key “Allow Arbitrary Loads“, type to boolean and value to YES.
Adding here my experience that hopefully might help somebody.
I was experiencing the same issue on Android emulator in Linux with hot reload. The code was correct as per accepted answer and the emulator could reach the internet (I needed a domain name).
Refreshing manually the app made it work. So maybe it has something to do with the hot reloading.
In My case, I am running a local Django backend in IP 127.0.0.1:8000
with Expo start.
Just make sure you have the server in public domain not hosted locally on your machine.
If it is hosted locally find the local IP address like 192.168.0.105 or something and use that
delete build folder projectfile\android\app\build and run project