Getting started with React Native, making http requests - react-native

Ok, so i just started out with React Native and googled the subject I was able to make this:
index.ios.js:
var React = require('react-native');
var { AppRegistry } = React;
var MarvelsApp = require('./components/start');
AppRegistry.registerComponent('MarvelsApp', () => MarvelsApp);
start.js:
'use strict';
var React = require('react-native');
var {
Text,
View,
Image,
TextInput,
StatusBar,
ListView
} = React;
var b = 'https://api.themoviedb.org/3/movie/popular?api_key=XXXXXX';
console.log('api url',b);
var stylesStart = React.StyleSheet.create({
container: {
backgroundColor: 'pink',
flex: 1
},
test: {
color: 'black',
fontSize: 22
}
});
var Start = React.createClass({
getInitialState() {
return {
movie: [],
};
},
componentDidMount: function() {
    this.fetchData();
  },
fetchData: function(){
fetch(b)
.then((response) => response.json())
.then((data) => {
console.log('Logging api response', data.results);
this.setState({
movie: data
});
})
.done();
},
render: function() {
return (
<View style={stylesStart.container}>
<StatusBar barStyle="light-content"/>
<Text style={stylesStart.test}>{this.state.movie.title}</Text>
</View>
);
}
});
module.exports = Start;
Basically what I am trying to achieve is to make an http request using the api from themoviedb.org. And then to display the retrieved data in the view.
So far I am able to pull in the data via the http request but I am not having much luck showing it in the view. Now this could be because I missed a step or because I am doing it wrong.
Please point out to me where or what I am doing wrong

You are accessing the wrong key from this.state, try logging this.state and see the structure of it. You'll probably need to call this.state.movie[0].title to see the title.

you init the state variable movie as an array
(return {movie: [],};),
but you use it as an object (this.state.movie.title),
finally, you update the state (this.setState({movie: data});)
make sure the type you use are you need.
if you want to render as a list , the state variable movie should be an array, or you just want to render a item ,it could be an object

Related

How to pass data back to previous screen with React Navigation 6. React Native

I am trying to pass the selected data i'm fetching from my rest api back to the previous screen. I followed the docs, but it's not working. Not sure what I am doing wrong.
I am updating the state w/ the selected data, and know it's working bc i am printing it out in console successfully.
This is the button that calls the function to navigate back, passing the state w/ selected data in Screen B:
<Pressable onPress={() => postSelected(selectedData)} >
<Text style={{fontSize: 13, color: 'white', fontWeight: '700', paddingRight: 5}}>{'Select'}</Text>
</Pressable>
This is the function (Screen B):
const postSelected = (selectedData) => {
navigation.navigate({
name: 'CreatePost',
params: { postData: selectedData },
merge: true
});
}
In Screen A, I have a useEffect that listens for the selected data from Screen B:
useEffect(() => {
if (route.params?.postData) {
console.log('Sent');
}
}, [route.params?.postData]);
But it's not receiving the data.
I was following these docs: https://reactnavigation.org/docs/params/
Appreciate any help!
You can use props.navigation.getParam("postData") Method to get params from navigation
The solution that worked for me and follows the best practices set by react-navigation is as follows
const MainScreen = ({ navigation }) => {
// Here is where the returned data is detected
React.useEffect(() => {
if (route.params?.post) {
// Do something with the data
}
}, [route.params?.post])
const openChildScreen = () => {
navigation.navigate('NewPostScreen')
}
}
// ---------------------
const NewPostScreen = ({ navigation }) => {
const goBackToMain = () => {
// This is the data to be returned to the main screen
const post = {
// ...
}
navigation.navigate('MainScreen', { post });
}
}

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

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!

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

In React Native, how can I access methods of one component from another component?

I'm trying to access a method of a React Native component from a different component. It is passed through props. Unfortunately, it seems like the components aren't providing their methods publicly. How can I get access to the method?
Have a look at the following, you'll see InsideView has this.props.myModal, which is a ShowMyModal component. However, it doesn't have access to the .openModal() method.
'use strict';
var React = require('react-native');
var {
AppRegistry,
ActionSheetIOS,
StyleSheet,
Text,
View,
} = React;
var InsideView = React.createClass({
makeItOpen: function() {
debugger;
this.props.myModal.openModal();
},
render: function() {
return (
<View>
<Text onPress={() => this.makeItOpen()}>Click me!</Text>
</View>
);
}
});
var ShowMyModal = React.createClass({
getInitialState: function() {
return {
isModalOpen: false,
}
},
openModal() {
this.setState({isModalOpen: true});
},
closeModal() {
this.setState({isModalOpen: false});
},
render: function() {
return (
<Text>isModalOpen = {String(this.state.isModalOpen)}</Text>
);
}
});
var AwesomeProject = React.createClass({
getInitialState: function() {
return {
myModal: <ShowMyModal />,
}
},
render: function() {
return (
<View style={{padding: 30}}>
<InsideView myModal={this.state.myModal}/>
{this.state.myModal}
</View>
);
},
});
AppRegistry.registerComponent('AwesomeProject', () => AwesomeProject);
Something like this should work:
'use strict';
var React = require('react-native');
var {
AppRegistry,
ActionSheetIOS,
StyleSheet,
Text,
TouchableOpacity,
View,
} = React;
var InsideView = React.createClass({
render: function() {
return (
<View>
<TouchableOpacity onPress={() => this.props.openModal()}><Text>Open modal!</Text></TouchableOpacity>
<TouchableOpacity onPress={() => this.props.closeModal()}><Text>Close modal!</Text></TouchableOpacity>
</View>
);
}
});
var ShowMyModal = React.createClass({
render: function() {
return (
<Text>isModalOpen = {String(this.props.isVisible)}</Text>
);
}
});
var SampleApp = React.createClass({
getInitialState: function() {
return {
isModalOpen: false
}
},
_openModal: function() {
this.setState({
isModalOpen: true
});
},
_closeModal() {
this.setState({
isModalOpen: false
});
},
render: function() {
return (
<View style={{padding: 30}}>
<InsideView openModal={this._openModal} closeModal={this._closeModal}/>
<ShowMyModal isVisible={this.state.isModalOpen}/>
</View>
);
},
});
AppRegistry.registerComponent('SampleApp', () => SampleApp);
I don't think it's a good idea to store the components in state. State should really be used for component's data rather than sub-components. Dave's solution above is good approach but it could be done a bit better as it moves the state of modal to the application (which is not very good to separate concerns). It's good if modal can keep it's own state and know if it's visible or not. Then openModal() and closeModal() can do some extra stuff as needed (as opposed to somehow reacting to change in visibility of ShowModal). You can also avoid those extra _openModal and _closeModal which are boilerplate.
I think it's best to use refs. Refs is standard way to refer to other components. See here for more details about refs https://facebook.github.io/react/docs/more-about-refs.html You can use refs as strings and refer to the component by that strings but it's kind of ugly as introduces global names which contradict the component approach of react. But you can also use callbacks as refs to set your internal components as fields. There is a nice and simple example of this is react's documentation: http://facebook.github.io/react-native/docs/direct-manipulation.html#forward-setnativeprops-to-a-child. I copy it here in case the documentation gets updated:
var MyButton = React.createClass({
setNativeProps(nativeProps) {
this._root.setNativeProps(nativeProps);
},
render() {
return (
<View ref={component => this._root = component} {...this.props}>
<Text>{this.props.label}</Text>
</View>
)
},
});
What happens here - the view in question has callback ref which sets this._root as the view's backing component. Then in any other place in the component you can use this._root to refer to it.
So in your case it could look like below (note that you need those anonymous arrow functions rather than passing the openModal / closeModal methods because at the time of rendering _modal is not yet set, you can only refer to it later using the anonymous methods).
// ...
// InsideView render (same as in Dave's solution)
<View>
<TouchableOpacity onPress={() => this.props.openModal()}><Text>Open modal!</Text></TouchableOpacity>
<TouchableOpacity onPress={() => this.props.closeModal()}><Text>Close modal!</Text></TouchableOpacity>
</View>
// ...
// Sample App render ...
<View style={{padding: 30}}>
<InsideView openModal={ () => this._modal.openModal() } closeModal={ () => this._modal.closeModal() } />
<ShowMyModal ref={component => this._modal = component} />
</View>
Then your initial ShowModal implementation can stay as it is - with it's own state and own openModal and showModal functions.