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.
Related
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()
})
})
})
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>
);
}
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>)
}
I'm trying to add a props inside a componentDidMount from redux.
If i try to log in in to my app with componentDidUpdate i'm able to see the data loaded, but if i close the app and after i try to re open it, i can't see the data.
class Profile extends Component {
constructor(props) {
super(props);
this.state = {
results: []
};
}
componentDidUpdate = () => {
this.getMyWeather();
};
getMyWeather = () => {
const {
getUser: { userDetails }
} = this.props;
axios
.get(
settings.host +
'my_api_url',
{
headers: { Authorization: 'Token ' + userDetails.token },
}
)
.then(({ data }) => {
this.setState({
results: data.results
});
})
.catch(error => alert(error));
};
render() {
return (
<View style={styles.container}>
{this.state.results &&
this.state.results.map((data, index) => (
<Text key={index}>{data.title}</Text>
))}
</View>
);
}
}
let mapStateToProps;
mapStateToProps = state => ({
getUser: state.userReducer.getUser
});
let mapDispatchToProps;
mapDispatchToProps = dispatch => ({
dispatch
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(Profile);
How i can fetch the data also after closing and re-open the app?
Try this way
async componentDidMount() {
// GET request using axios with async/await
const {userDetails} = this.props.getUser; <-- Try this way -->
const data = await this.getMyWeather(userDetails);
this.setState({
results: data
});
}
getMyWeather = async (userDetails) => {
await axios
.get(
settings.host +
'my_api_url',
{
headers: { Authorization: 'Token ' + userDetails.token },
}
)
.then(({ data }) => {
return data.results;
})
.catch(error => alert(error));
};
Why to save to token in your redux in the first place?
personally I save it in local storage it's easy.
as you know redux is a state management of react this is mean when the you close the website the data store in redux die and because of this I think you should save in the local storage so you can get access to it really easy.
If you save the JWT in the DB you just need in the useEffect in the app.js call the action in redux that extract the JWT and save it
I have been trying to use redux and redux-thunk to help get a json file from a api and have been getting a warning stating that action must be a plain object. I am really confused as to where the issue is in the code. i have tried following many other stackoverflow posts and a couple of guides online and have not really got a good grasp of where I am going wrong. I understand that this is a problem with how I am referencing async and dispatch but do not know how to fix it.
This is the function that causes the warning to appear in the simulator
export const fetchData = url => {
console.log("Should enter async dispatch");
return async (dispatch) => {
dispatch(fetchingRequest());
fetch("https://randomuser.me/api/?results=10")
.then(response => {
if (response.ok) {
let json = response.json();
dispatch(fetchingSuccess(json));
console.log("JSON", json);
}
})
.catch(error => {
dispatch(fetchingFailure(error));
console.log("Error", error);
});
};
};
Here is the output in the console
Possible Unhandled Promise Rejection (id: 0):
Error: Actions must be plain objects. Use custom middleware for async actions.
Error: Actions must be plain objects. Use custom middleware for async actions.
Edit: including setup of middleware
I have the middleware setup in the index.js file of my app
index.js
import { AppRegistry } from "react-native";
import App from "./App";
import { name as appName } from "./app.json";
import { Provider } from "react-redux";
import React, { Components } from "react";
import { createStore, applyMiddleware } from "redux";
import appReducer from "./src/data/redux/reducers/appReducer";
import thunk from "redux-thunk";
const createStoreWithMiddleware = applyMiddleware(thunk)(createStore);
const store = createStoreWithMiddleware(appReducer);
console.log("Store", store.getState());
const AppContainer = () => (
<Provider store = {store}>
<App />
</Provider>
);
AppRegistry.registerComponent(appName, () => AppContainer);
I learned this implementation of store from a Youtube Tutorial.
Edit 2: Adding in the fetchData call
I call fetchData in a _onPress function like this
_onPress = () => {
const {fetchData} = this.props;
let url = "https://randomuser.me/api/?results=10";
fetchData(url);
console.log("should have fetched");
};
this is how my app has been connected to redux
const mapStateToProps = state => {
return { response: state };
};
const mapStateToDispatch = dispatch => ({
fetchData: url => dispatch(fetchData(url)),
});
export default connect(
mapStateToProps,
mapStateToDispatch
)(SearchScreen);
these are the action in my app
export const fetchingRequest = () => {
{
type: FETCHING_REQUEST;
}
};
export const fetchingSuccess = json => {
{
type: FETCHING_SUCCESS;
payload: json;
}
};
export const fetchingFailure = error => {
{
type: FETCHING_FAILURE;
payload: error;
}
};
I was able to figure out the problem thanks to working through the steps in the comments thanks to Michael Cheng. I ended up finding that the problem was that i had actions with plain objects but they were not returning anything.
The original actions were
export const fetchingRequest = () => {
{
type: FETCHING_REQUEST;
}
};
export const fetchingSuccess = json => {
{
type: FETCHING_SUCCESS;
payload: json;
}
};
export const fetchingFailure = error => {
{
type: FETCHING_FAILURE;
payload: error;
}
};
to this
export const fetchingRequest = () => {
return {
type: FETCHING_REQUEST
}
};
export const fetchingSuccess = json => {
return {
type: FETCHING_SUCCESS,
payload: json
}
};
export const fetchingFailure = error => {
return {
type: FETCHING_FAILURE,
payload: error
};
};
with including the return for each action