React: how to initialise redux state before rendering component? - react-native

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.

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();
// ...
}

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!

Save react-native-check-box status after reload

I am building a React Native Iphone App.I have a checkbox "Remember me" in Login page, which I want to set to remember the username and password in order to login.I want to save the status of checkbox even after reload(Once it is ticked it should persist till it is ticked-off by the user).Below is my code.
index.js :
import React, { Component } from 'react';
import { View,KeyboardAvoidingView, Text, StyleSheet, Dimensions} from 'react-
native';
import CheckBox from 'react-native-check-box';
import AsyncStorage from '#react-native-community/async-storage';
export default class index extends Component{
constructor() {
super();
this.state = {
status: false
};
toggleStatus = async() =>{
this.setState({
status: !this.state.status
});
AsyncStorage.setItem("myCheckbox",JSON.stringify(this.state.status));
}
}
componentWillMount(){
AsyncStorage.getItem('myCheckbox').then((value) => {
this.setState({
status: (value === 'true')
});
});
}
render() {
return (
<KeyboardAvoidingView
style={{ flex: 1, backgroundColor: 'white', justifyContent: 'flex-end'}}
behavior="padding"
keyboardVerticalOffset={50}
enabled>
<Text>{typeof this.state.status +' : '+ this.state.status}</Text>
<CheckBox
style={{flex: 1,paddingLeft:100,paddingTop:20}}
onClick={()=>{
this.setState({
isChecked:!this.state.isChecked
})
toggleStatus(this)
}}
isChecked={this.state.isChecked}
rightText={"Remember me"}
/>
</KeyboardAvoidingView>
);
}
}
index.navigationOptions = {
headerTitle: ''
};
const styles = StyleSheet.create({
});
I could save the status but not set it after reload.I have tried some techniques using the stackoverflow logics, but dint give me proper result.Can anyone help me to set the checkbox.Thanks in advance.
I think you are making a mistake in your toggle method. async doesn't work here (Also we need to use await with async) you should write your code like this. setState take time to save the state so you need to use its callback function which called after the state saved.
I am showing 2 ways here but I prefer the first one.
toggleStatus =() =>{
this.setState({
status: !this.state.status
}, () => AsyncStorage.setItem("myCheckbox",JSON.stringify(this.state.status)));
}
OR
You can do like
toggleStatus = () =>{
AsyncStorage.setItem("myCheckbox",JSON.stringify(!this.state.status));
this.setState({
status: !this.state.status
});
}

Getting a spinning plank screen and Error: Camera is not ready yet. Wait for 'onCameraReady' callback using expo Camera component

I'm new to web development and I'm trying to build an image recognition app using expo for testing. My code for the camera is below. On screen load, I get a black screen (not the camera) with my "capture" button. When I click on capture, I get the error:
Unhandled promise rejection: Error: Camera is not ready yet. Wait for 'onCameraReady' callback.
My code is below
import { Dimensions, Alert, StyleSheet, ActivityIndicator } from 'react-native';
// import { RNCamera } from 'react-native-camera';
import CaptureButton from './CaptureButton.js'
import { Camera } from 'expo-camera';
export default class AppCamera extends React.Component {
constructor(props){
super(props);
this.state = {
identifiedAs: '',
loading: false
}
}
takePicture = async function(){
if (this.camera) {
// Pause the camera's preview
this.camera.pausePreview();
// Set the activity indicator
this.setState((previousState, props) => ({
loading: true
}));
// Set options
const options = {
base64: true
};
// Get the base64 version of the image
const data = await this.camera.takePictureAsync(options)
// Get the identified image
this.identifyImage(data.base64);
}
}
identifyImage(imageData){
// Initialise Clarifai api
const Clarifai = require('clarifai');
const app = new Clarifai.App({
apiKey: '8d5ecc284af54894a38ba9bd7e95681b'
});
// Identify the image
app.models.predict(Clarifai.GENERAL_MODEL, {base64: imageData})
.then((response) => this.displayAnswer(response.outputs[0].data.concepts[0].name)
.catch((err) => alert(err))
);
}
displayAnswer(identifiedImage){
// Dismiss the acitivty indicator
this.setState((prevState, props) => ({
identifiedAs:identifiedImage,
loading:false
}));
// Show an alert with the answer on
Alert.alert(
this.state.identifiedAs,
'',
{ cancelable: false }
)
// Resume the preview
this.camera.resumePreview();
}
render () {
const styles = StyleSheet.create({
preview: {
flex: 1,
justifyContent: 'flex-end',
alignItems: 'center',
height: Dimensions.get('window').height,
width: Dimensions.get('window').width,
},
loadingIndicator: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
}
});
return (
<Camera ref={ref => {this.camera = ref;}}style={styles.preview}>
<ActivityIndicator size="large" style={styles.loadingIndicator} color="#fff" animating={this.state.loading}/>
<CaptureButton buttonDisabled={this.state.loading} onClick={this.takePicture.bind(this)}/>
</Camera>
)
}
}```
Could someone kindly point me in the right direction to fix this error?
https://docs.expo.dev/versions/latest/sdk/camera/#takepictureasyncoptions
Note: Make sure to wait for the onCameraReady callback before calling this method.
So, you might resolve if you add onCameraReady props to Camera component like this document.
I'm facing issue like this, and it is not resolved now... I hope my advice works well.

how to pass form data from screen1 to screen2 in react native?

how to pass form data from screen1 to screen2 in react native ? I have following code in scrren1 I want posted amount data in screen2. Please let me know how can I pass data on screen2 and receive it in react native?
import React, { Component } from 'react';
import { Button, View, Text, StyleSheet } from 'react-native';
import t from 'tcomb-form-native'; // 0.6.9
const Form = t.form.Form;
const User = t.struct({
amount: t.String,
});
const formStyles = {
...Form.stylesheet,
formGroup: {
normal: {
marginBottom: 10
},
},
controlLabel: {
normal: {
color: 'blue',
fontSize: 18,
marginBottom: 7,
fontWeight: '600'
},
// the style applied when a validation error occours
error: {
color: 'red',
fontSize: 18,
marginBottom: 7,
fontWeight: '600'
}
}
}
const options = {
fields: {
amount: {
label: "Enter Amount You want to Top up",
error: 'Please add amount to proceed ahead!'
},
},
stylesheet: formStyles,
};
class HomeScreen extends Component {
static navigationOptions = {
title: 'Home',
};
handleSubmit = () => {
const value = this._form.getValue();
console.log('value: ', value);
}
render() {
return (
<View style={styles.container}>
<Form
ref={c => this._form = c}
type={User}
options={options}
/>
<Button
title="Pay Now"
onPress={this.handleSubmit}
/>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
justifyContent: 'center',
marginTop: 50,
padding: 20,
backgroundColor: '#ffffff',
},
});
export default HomeScreen;
It depends if you want to pass data between Parent to Child, Child to Parent or Between Siblingsā€Š
I suggest you to read Passing Data Between React Components, old but this article did help me to understand the logic behind passing data as it's not as easy to implement as in other programming languages.
Excerpt using props:
class App extends React.Component {
render() {
[... somewhere in here I define a variable listName
which I think will be useful as data in my ToDoList component...]
return (
<div>
<InputBar/>
<ToDoList listNameFromParent={listName}/>
</div>
);
}
}
Now in the ToDoList component, use this.props.listNameFromParent to access that data.
You have many ways to send informations from one screen to another in React Native.
eg.
Use React Navigation to navigate between your scenes. You will be able to pass params to your components, which will be accessible in the navigation props when received.
this.props.navigation.navigate({routeName:'sceneOne', params:{name} });
You can also send directly props to a component, and treat them in it. In your render section of your first component, you could have something like this :
<myComponent oneProps={name}/>
In that example, you will receive the props "oneProps" in your second component and you will be able to access it that way :
type Props = {
oneProps: string,
}
class myComponent extends React.Component<Props> {
render() {
console.log('received sent props', oneProps);
return (
<View> // display it
<Text>{this.props.oneProps}</Text>
</View>
);
};
}
These are only two effective solutions, but there are a lot more.
Hope it helped you :)
Have a good day