How to test a component that renders asynchronously after a call - react-native

Suppose I have a component that loads its content when an asynchronous call returns succesfuly:
const MyScreen = () => {
let userData: userDataResponse;
const [email, setEmail] = useState("");
const [firstTime, setFirstTime] = useState(true);
async function localGetUserData() {
userData = await getUserData();
setEmail(userData.email);
setFirstTime(false);
}
useEffect(() => {
localGetUserData();
}, []);
if (firstTime) {
return <Text>Cargando...</Text>;
}
return (
<SafeAreaView style={styles.formStyling}>
When the data is available, it sets a state variable so the real content renders
If I want to test it, I think I should mock the getUserData so the mocked function returns a mocked email, say {email: a#b.c}
What would be a good approach to achieve this?

Assuming following component setup (as I cannot see whole component):
myScreenUtils.js
export const getUserData = async () => {
return Promise.resolve('original implementation')
}
MyScreen.jsx
import { useState, useEffect } from "react";
import { getUserData } from './myScreenUtils.js'
const MyScreen = () => {
let userData;
const [email, setEmail] = useState("");
const [firstTime, setFirstTime] = useState(true);
async function localGetUserData() {
userData = await getUserData();
setEmail(userData.email);
setFirstTime(false);
}
useEffect(() => {
localGetUserData();
}, []);
if (firstTime) {
return <div>Cargando...</div>;
}
return (
<div>{email}</div>
)
};
export default MyScreen;
You can write following tests:
import { screen, render, waitFor, waitForElementToBeRemoved } from '#testing-library/react';
import MyScreen from "../MyScreen";
import * as utils from '../myScreenUtils';
describe('MyScreen', () => {
it('the text is displayed and then removed', async () => {
jest.spyOn(utils, 'getUserData').mockResolvedValue({ email: 'mocked value' });
render(<MyScreen />);
expect(screen.getByText('Cargando...')).toBeInTheDocument();
await waitForElementToBeRemoved(() => screen.queryByText('Cargando...'))
})
it('the text email is fetched and displayed', async () => {
jest.spyOn(utils, 'getUserData').mockResolvedValue({ email: 'mocked value' });
render(<MyScreen />);
await waitFor(() => {
expect(screen.getByText('mocked value')).toBeInTheDocument()
})
})
})

Related

How to implement splash screen properly in a component which have hooks running?

Inside App.js I have auth validation (i am using useState, useMemo, useEffect) but when tried to impement splash screen and following Splas screen Dos I am getting Rendered more hooks than during the previous render. So following Rules of Hooks I put at top level useEffect and useState but now I am getting a new error Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in %s.%s, a useEffect cleanup function, in App I see I need to cancel async functions but I need them to request the server and validate users.
This is how my code was before implementing Splash screen:
export default function App() {
const [auth, setAuth] = useState(undefined);
useEffect(() => {
(async () => {
const token = await getTokenApi();
if (token) {
setAuth({
token,
idUser: jwtDecode(token).id,
});
} else {
setAuth(null);
}
})();
}, []);
const login = (user) => {
setTokenApi(user.jwt);
setAuth({
token: user.jwt,
idUser: user.user.id,
});
};
const logout = () => {
if (auth) {
removeTokenApi();
setAuth(null);
}
};
const authData = useMemo(
() => ({
auth,
login,
logout,
}),
[auth]
);
if (auth === undefined) return null;
return (
<AuthContext.Provider value={authData}>
<PaperProvider>{auth ? <AppNavigation /> : <Auth />}</PaperProvider>
</AuthContext.Provider>
);
This is how i got it now
export default function App() {
const [auth, setAuth] = useState(undefined);
useEffect(() => {
(async () => {
const token = await getTokenApi();
if (token) {
setAuth({
token,
idUser: jwtDecode(token).id,
});
} else {
setAuth(null);
}
})();
}, []);
const [appIsReady, setAppIsReady] = useState(false);
useEffect(() => {
async function prepare() {
try {
await SplashScreen.preventAutoHideAsync();
await Font.loadAsync(Entypo.font);
await new Promise((resolve) => setTimeout(resolve, 4000));
} catch (e) {
console.warn(e);
} finally {
setAppIsReady(true);
}
}
prepare();
}, []);
const login = (user) => {
setTokenApi(user.jwt);
setAuth({
token: user.jwt,
idUser: user.user.id,
});
};
const logout = () => {
if (auth) {
removeTokenApi();
setAuth(null);
}
};
const authData = useMemo(
() => ({
auth,
login,
logout,
}),
[auth]
);
if (auth === undefined) return null;
const onLayoutRootView = useCallback(async () => {
if (appIsReady) {
await SplashScreen.hideAsync();
}
}, [appIsReady]);
if (!appIsReady) {
return null;
}
return (
<View onLayout={onLayoutRootView}>
<AuthContext.Provider value={authData}>
<PaperProvider>{auth ? <AppNavigation /> : <Auth />}</PaperProvider>
</AuthContext.Provider>
</View>
);
}

How to pass the parameter to another screen using axios?

I'm doing the verification of the phone number, and I have to pass the phone number to the other checkCode.js component.
I have seen examples that pass it navigate() as a pramas, but how can I receive it in another component.
register.js
const SignUp = ({ navigation }) => {
const [phoneNumber, setPhoneNumber] = useState('');
let register = "https://app.herokuapp.com/api/v1/auth/register"
let sendVerification = "https://app.herokuapp.com/api/v1/auth/sendVerification-otp"
const signUp = () => {
const userParams = {
phone: phoneNumber,
};
const requestOne = axios.post(register, userParams)
const requestTwo = axios.post(sendVerification, userParams)
axios
.all([requestOne, requestTwo], userParams)
.then(axios.spread((...responses) => {
navigation.navigate('CodeVerification')
}))
.catch((err) => {
console.log('the error:', err.message);
})
}
checkCode.js
export default function CodeVerification({navigation}) {
//need phoneNumber param in this component
const [code, setCode] = useState('');
const confirm = () =>{
const userParams = {
phone: "+11111111",
code:code,
};
axios
.post('https://app.herokuapp.com/api/v1/auth/sendVerification-otp', userParams)
.then((response) =>{
console.log('response', response.data);
navigation.navigate('Welcome')
})
.catch((error) => {
console.log('the error:', error.message);
});
};
How can I pass it?
This might help
register.js
const SignUp = ({ navigation }) => {
// existing code remains the same
const signUp = () => {
....
axios
.all([requestOne, requestTwo], userParams)
.then(
axios.spread((...responses) => {
// send params like this
navigation.navigate("CodeVerification", {phone: phoneNumber});
})
)
.catch((err) => {
console.log("the error:", err.message);
});
};
};
checkCode.js
export default function CodeVerification({ route, navigation }) {
// get phoneNumber from props
const {phone} = route.params; // UPDATED this line
const [code, setCode] = useState("");
....
}
You can use Context Api
Context api is commonly used for transferring data to another component.

How to call a function declared in App.js

I have a react native project that uses react-navigation. I have these two screens that are part of a stack navigator. I want to call all API related functions in App.js or the stack navigator rather than directly on a screen. I would also like to use data in the two screens. How can I do this?
App.js
import fetchData from './Data';
export default function App() {
const [data, setData] = useState([]);
useEffect(() => {
const fetchAPI = async () => {
setData1WeekCases(await fetchData());
};
fetchAPI();
}, [setData1WeekCases]);
}
Data.tsx
export const fetchData = async () => {
try {
const {
data: { countries },
} = await axios.get("https://covid19.mathdro.id/api/countries");
return countries.map((country) => country.name);
} catch (error) {
console.log(error);
}
};
StackNavigator.tsx
const AppStack = createNativeStackNavigator();
const MainStackNavigator = () => {
return (
<AppStack.Navigator>
<AppStack.Screen
name="HomeScreen"
component={HomeScreen}
options={{
title: "Home",
}}
/>
<AppStack.Screen
name="DataScreen"
component={DataScreen}
options={{
headerBackTitle: "Summary",
title: "Data",
}}
/>
<AppStack.Navigator>
)
}
First of all to access data globally like in your case between screens, you have to use state management tool like Redux or Context. You can find many tutorials for this on youtube if you can't figure it out using the docs.
Secondly if you want to do all the fetching in a separate file then you can create an axios instance in a separate file like this :
import axios from "axios";
import AsyncStorage from "#react-native-async-storage/async-storage";
const axiosClient = axios.create();
axiosClient.defaults.baseURL = "API_URL";
axiosClient.defaults.headers = {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
};
//All request will wait 2 seconds before timeout
axiosClient.defaults.timeout = 2000;
axiosClient.defaults.withCredentials = true;
export default axiosClient;
axiosClient.interceptors.request.use(
async config => {
const token = await AsyncStorage.getItem('token');
if (token) {
config.headers.Authtoken = JSON.parse(token);
}
return config;
},
error => {
return console.log(error);
},
);
export function getRequest(URL) {
return axiosClient
.get(URL)
.then((response) => response)
.catch((err) => err);
}
export function postRequest(URL, payload) {
//! step for x-www-form-urlencoded data
const params = new URLSearchParams(payload);
return axiosClient.post(URL, params).then((response) => response);
}
export function patchRequest(URL, payload) {
return axiosClient.patch(URL, payload).then((response) => response);
}
export function deleteRequest(URL) {
return axiosClient.delete(URL).then((response) => response);
}
Fetch data from the api then change the state using redux to get the response in every screen.

redux toolkit and reselect

I want to use reselect but I do not understand it correctly yet.
If I want to filter anything then I can do this:
const selectNumCompletedTodos = createSelector(
(state) => state.todos,
(todos) => todos.filter((todo) => todo.completed).length
)
But if I fetch, how does it look then? (I use useSelector and not mapToProps)
My Code:
Login:
imports
...
const Login = ({ navigation, route }) => {
const dispatch = useDispatch();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [passwordShown, setPasswordShown] = useState(true);
const handleGoBack = () => {
navigation.goBack();
};
const handleLogin = async () => {
const payload = {
email,
password
};
dispatch(request(payload));
};
return (
<View>
<TouchableOpacity onPress={handleLogin}>
<Text>click to fetch!</Text>
</TouchableOpacity>
</View>
)
authSaga.js
import { all, call, put, take, takeEvery, takeLatest } from 'redux-saga/effects';
import { request, authSuccess, authFailure } from '../slice/authSlice';
import authAPI from '../../api/auth';
import * as SecureStore from 'expo-secure-store';
function* auth({ payload }) {
const data = yield call(authAPI, payload);
yield put(authSuccess(data.user));
}
function* watcher() {
yield takeEvery(request.type, auth);
}
export default function* () {
yield all([watcher()]);
}
reducers:
import { combineReducers } from "redux";
import authReducer from './slice/authSlice';
const rootReducer = combineReducers({
user: authReducer,
});
export default rootReducer;
Slice:
import { createSlice } from '#reduxjs/toolkit';
const authSlice = createSlice({
name: 'user',
initialState: {
loading: false,
data: [],
error: null
},
reducers: {
request(state) {
state.loading = true;
},
authSuccess(state, action) {
state.loading = false;
state.data = action.payload;
},
authFailure(state, action) {
state.loading = false;
state.error = action.payload;
}
}
});
export const { request, authSuccess, authFailure } = authSlice.actions;
export default authSlice.reducer;
can anyone help me ?
................................................................................

Why data is not loading from this dispatch action?

I am trying to learn redux.
I watch some tutorials and follow along with them. These tutorials are with class component.
So I try to change these into functional component.
Since I am just learning and not trying to make a big project I put actions, reducers and types into 1 file.
This is that file
import axios from 'axios';
export const FETCH_NEWS = 'FETCH_NEWS';
// Reducer
const initialState = {
newsList: [],
};
export const articlesReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_NEWS:
return {...state, newsList: action.payload};
default:
return state;
}
};
export const fetchNews = () => (dispatch) => {
axios
.get('https://jsonplaceholder.typicode.com/users')
.then((res) => {
dispatch({
type: FETCH_NEWS,
payload: res.data,
});
})
.catch((err) => {
console.log(err);
});
};
So I am using fetchNews props in News component
News component is like this
import { fetchNews }from '../../ducks/modules/Articles'
useEffect(() => {
fetchNews();
console.log('##############################')
console.log(newsList)
console.log('##############################')
},[])
const News = ({navigation, newsList, fetchNews}) => {
return (<View> .... </View>)
}
News.propTypes = {
fetchNews: PropTypes.func.isRequired,
newsList: PropTypes.array.isRequired
}
const mapStateToProps = state => {
return {
newsList: state.articlesReducer.newsList
}
}
export default connect(mapStateToProps, { fetchNews })(News);
As you can see I am console.logging in the useEffect hooks , I am console logging because no data are being loaded in the device
Here is a picture of empty array when component is mounted
My store component is like this
const reducer = combineReducers({
articlesReducer
});
const store = createStore(reducer, applyMiddleware(thunk,logger));
You are not dispatching the action correctly. I have added simpler way to use redux with function based components. You don't need to use connect.
export const fetchNews = () => (dispatch) => {
axios
.get('https://jsonplaceholder.typicode.com/users')
.then((res) => {
dispatch({
type: FETCH_NEWS,
payload: res.data,
});
})
.catch((err) => {
console.log(err);
});
};
export const selectNewsList = (state) => state.newsList; // this is known as a selector.
And your view will be:
import { useSelector, useDispatch } from 'react-redux';
import { fetchNews, selectNewsList }from '../../ducks/modules/Articles'
const News = () => {
const newsList = useSelector(selectNewsList);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchNews());
},[])
console.log(newsList); // This will print empty array first, but will print again as data is populated.
return (<View> .... </View>)
}