Async custom hook from within useEffect - react-native

When kept in the component body, the following code works fine. Inside useEffect, it checks the asyncstorage and dispatches an action (the function is longer but other checks/dispatches in the function are of the same kind - check asyncstorage and if value exists, dispatch an action)
useEffect(() => {
const getSettings = async () => {
const aSet = await AsyncStorage.getItem('aSet');
if (aSet) {
dispatch(setASet(true));
}
};
getSettings();
}, [dispatch]);
I'm trying to move it to a custom hook but am having problems. The custom hook is:
const useGetUserSettings = () => {
const dispatch = useDispatch();
useEffect(() => {
const getSettings = async () => {
const aSet = await AsyncStorage.getItem('aSet');
if (aSet) {
dispatch(setASet(true));
}
};
getSettings();
}, [dispatch]);
};
export default useGetUserSettings;
Then in the component where I want to call the above, I do:
import useGetUserSettings from './hooks/useGetUserSettings';
...
const getUserSettings = useGetUserSettings();
...
useEffect(() => {
getUserSettings();
}, [getUserSettings])
It returns an error:
getUserSettings is not a function. (In 'getUserSettings()', 'getUserSettings' is undefined
I've been reading rules of hooks and browsing examples on the internet but I can get it working. I've got ESlint set up so it'd show if there were an invalid path to the hook.

Try the following.
useEffect(() => {
if (!getUserSettings) return;
getUserSettings();
}, [getUserSettings]);

The hook doesn't return anything, so it's not surprising that the return value is undefined ;)

Related

React native UseEffect and async issue

I'm trying to get a StoreKey from firestore (v9), and put it inside another collection of DB as a path.
for example, get storeKey (132, for example) and put inside
collection(db, 'store', storeKey, 'coffeeDB') to access specific sub collection. I put two function (1: getData (storeKey), 2: access to sub collection) into UseEffect so that it can run when it's mounted.
However, I found UseEffect runs twice, initial storeKey shows Array [], and the next run gets proper value which is 132. So, I'm having an error due to the first run.
I guess it's because the second function inside UseEffect does not wait for getData function to watch the data, but not too sure.
How can I resolve this issue??
const getData = async(setStoreKey, setName) => {
console.log('xxxx')
const auth = getAuth();
const user = auth.currentUser;
if(user !== null){
const email = user.email;
const UserInfo = await getDoc(doc(db, 'users', email));
if(UserInfo.exists()){
setStoreKey(UserInfo.data().storeKey)
setName(UserInfo.data().name);
}
else{
console.log('None')
}
return
}
}
T
const StockScreen = ({ navigation }) => {
const [storeKey, setStoreKey] = useState([]);
const [userName, setName] = useState([]);
const [coffeeStock, setCoffeeStock] = useState([]);
useEffect(() => {
getData(setStoreKey, setName);
const unsub = onSnapshot(collection(db, 'Store', storeKey, 'coffeeDB'), (snapshot) => {
setCoffeeStock(snapshot.docs.map((doc) => ({
id: doc.id,
data: doc.data(),
number: doc.data(),
})));
});
return unsub;
}, [storeKey]);
Just remove dependency from second argument in useEffect and pass blank array
useEffect(() => {
// your code
}, []);
It will run only once when your component is loaded. It is similar to componentDidMount of class component.
The reason your useEffect run twice is because your storeKey state changing in getData(setStoreKey, setName) function. So what you can do here if you want to call getData() function once is to declare it on a separate useEffect function like:
useEffect(() => {
getData(setStoreKey, setName); //call your getData function once
}, []);
And what I see is you need to update StoreKey every time for the unsub listener so with that above useEffect call another useEffect whenever the StoreKey dependency change like:
useEffect(() => {
getData(setStoreKey, setName); //call your getData function once
}, []);
useEffect(() => { //another useEffect whenever storeKey changes
const unsub = onSnapshot(collection(db, 'Store', storeKey, 'coffeeDB'), (snapshot) => {
setCoffeeStock(snapshot.docs.map((doc) => ({
id: doc.id,
data: doc.data(),
number: doc.data(),
})));
});
return unsub;
}, [storeKey]);
Hope this works for you.

React Native testing - act without await

Below test is passing but I get the following warning twice and I don't know why. Could someone help me to figure it out?
console.error
Warning: You called act(async () => ...) without await. This could lead to unexpected testing behaviour, interleaving multiple act calls and mixing their scopes. You should - await act(async () => ...);
at printWarning (../../node_modules/react-test-renderer/cjs/react-test-renderer.development.js:120:30)
at error (../../node_modules/react-test-renderer/cjs/react-test-renderer.development.js:92:5)
at ../../node_modules/react-test-renderer/cjs/react-test-renderer.development.js:14953:13
at tryCallOne (../../node_modules/react-native/node_modules/promise/lib/core.js:37:12)
at ../../node_modules/react-native/node_modules/promise/lib/core.js:123:15
at flush (../../node_modules/asap/raw.js:50:29)
import { fireEvent } from '#testing-library/react-native'
import { renderScreen } from 'test/render'
describe('screens/home', () => {
it('should render and redirect to the EventScreen', async () => {
const {
getByA11yLabel,
findByA11yLabel,
findAllByA11yLabel,
toJSON
} = renderScreen('Main')
expect(toJSON()).toMatchSnapshot('Default render')
const title = 'New event'
const titleInput = getByA11yLabel('event.title')
// Change title - sync fn
fireEvent.changeText(titleInput, title)
// Create button should be visible
const createButton = await findByA11yLabel('event.create')
expect(titleInput.props.value).toBe(title)
expect(createButton).toBeTruthy()
expect(toJSON()).toMatchSnapshot('Change title')
// Create event - async fn
fireEvent.press(createButton)
// The app should be redirected to the EventScreen
const titleInputs = await findAllByA11yLabel('event.title')
const upsertButton = await findByA11yLabel('event.upsert')
expect(toJSON()).toMatchSnapshot('Create event')
expect(titleInputs).toHaveLength(2)
expect(titleInputs[0].props.value).toBe('') // #MainScreen
expect(titleInputs[1].props.value).toBe(title) // #EventScreen
expect(upsertButton).toBeTruthy()
})
})
As far as I know, there is no need to wrap fireEvent with an act- link
findBy* also are automatically wrapped with act - link
Related issue in GitHub is still open
Dependencies:
react: 16.13.1
expo: 39.0.4
jest: 26.6.3
ts-jest: 26.4.4
jest-expo: 39.0.0
#testing-library/jest-native: 3.4.3
#testing-library/react: 11.2.2
#testing-library/react-native: 7.1.0
react-test-renderer: 16.13.1
typescript: 4.1.2
If you've exhausted all other debugging efforts and are pretty sure your code is written correctly, it may be related to react-native/jest-preset replacing global.Promise with a mock (see issue).
The solution to the problem, in this case, is to override/patch the jest preset to first save the original global Promise, apply the react-native/jest-preset and then restore the original Promise (overwriting the mocked version). This allowed me to use await in the tests that were unrelated to rendering without triggering the dreaded
console.error
Warning: You called act(async () => ...) without await. This could lead to unexpected testing behaviour, interleaving multiple act calls and mixing their scopes. You should - await act(async () => ...);
This snippet shows one way to perform this patch: https://github.com/sbalay/without_await/commit/64a76486f31bdc41f5c240d28263285683755938
I was facing the same problem. For my case I was using useEffect in my component. And while test it prompted me to wrap the rendering inside an act() call. Once I did that i.e. act(async () => ...) my initial problem was solved but I was getting the above mentioned error (Warning: You called act(async () => ...) without await.). I had to use await act(async () => ...) in my test to fix that. Though I am still not sure why it was required.
For reference I am adding a complete example component and corresponding test using await act(async () => ...);
LocationComponent.tsx
/** #jsx jsx */
import { jsx } from 'theme-ui';
import { FunctionComponent, useEffect, useState } from 'react';
type Coordinate = {
latitude: number;
longitude: number;
};
const LocationComponent: FunctionComponent<any> = () => {
const [coordinate, setCoordinate] = useState<Coordinate>();
const [sharedLocation, setSharedLocation] = useState<boolean>();
useEffect(() => {
let mounted = true;
if (!coordinate && navigator) {
navigator.geolocation.getCurrentPosition(function (position) {
setCoordinate({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
});
});
navigator.permissions
.query({ name: 'geolocation' })
.then(function (result) {
if (mounted) setSharedLocation(result.state === 'granted');
});
}
return () => (mounted = false);
});
return (
<>
<div>Location shared:{sharedLocation ? 'Yes' : 'No'}</div>
<div>Latitude:{coordinate?.latitude}</div>
<div>Longitude:{coordinate?.longitude}</div>
</>
);
};
export default LocationComponent;
LocationComponent.spec.tsx
import React from 'react';
import { render, waitFor } from '#testing-library/react';
import { act } from 'react-dom/test-utils';
import LocationComponent from '../../../../../src/components/scheduler/location/LocationComponent';
const TEST_COORDS = {
latitude: 41.8817089,
longitude: -87.643301,
};
global.navigator.permissions = {
query: jest
.fn()
.mockImplementationOnce(() => Promise.resolve({ state: 'granted' })),
};
global.navigator.geolocation = {
getCurrentPosition: jest.fn().mockImplementationOnce((success) =>
Promise.resolve(
success({
coords: TEST_COORDS,
})
)
),
};
describe("Location Component when location share is 'granted'", () => {
it('should display current location details', async () => {
await act(async () => {
const { getByText } = render(<LocationComponent />);
/*expect(
await waitFor(() => getByText('Location shared:Yes'))
).toBeInTheDocument();*/
expect(
await waitFor(() => getByText('Latitude:41.8817089'))
).toBeInTheDocument();
expect(
await waitFor(() => getByText('Longitude:-87.643301'))
).toBeInTheDocument();
});
});
});

React Natie, When I use useState data on componentWillUnmount, just getting first data

When I use useState data inside useEffect return function (componentWillUnmount), I am just getting first data,
I am changing note values on textview and when i go back another screen, useEffect return function working well but note values is "". Why this happining and how can i solve it?
const [note, setNote] = useState("");
useEffect(() => {
getContent();
return () =>{
saveToStorage({note:note}, "componentWillUnmount");
};
}, []);
const saveToStorage = (data) =>{
console.log(data); // note:""
}
useEffect will get the note reference only once since you have added [] as dependencies instead give notes as dependencies so it will be called evertime so it can get updated note reference
code:
const [note, setNote] = useState("");
React.useEffect(() => {
if (condition to check wether its first time) {
// getNotes()
}
return () => {
saveToStorage({ note: note }, 'componentWillUnmount');
}
}, [note]);
const saveToStorage = (data) =>{
console.log(data); // note:""
}

React Native hooks - correct use of useEffect()?

I'm new to hooks and ran across this setup on SO and wanted to confirm that this is the correct pattern. I was getting the RN "unmounted component" leak warning message before and this seemed to solve it. I'm trying to mimic in some way compnentDidMount. This is part of a phone number verify sign up flow and onMount I want to just check for navigation and then fire off a side effect, set mounted true and then unmount correctly.
const SMSVerifyEnterPinScreen = ({ route, navigation }) => {
const [didMount, setDidMount] = useState(false)
const { phoneNumber } = route.params
useEffect(() => {
if(navigation) {
signInWithPhoneNumber(phoneNumber)
setDidMount(true)
}
return () => setDidMount(false)
}, [])
if (!didMount) { return null }
async function signInWithPhoneNumber(phoneNumber) {
const confirmation = await auth().signInWithPhoneNumber('+1'+phoneNumber)
...
}
return (
...
)
}
RN 0.62.2 with react-nav 5 - thanks!
Since signInWithPhoneNumber is a async function and will setState you will see warning it the component is unmounted before the response is available
In order to handle such scenarios you can keep a variable to keep track whether its mounted or not and then only set state is the mounted variable is true
However you do not need to return null if component has unmounted since that doesn't accomplish anything. The component is removed from view and will anyways not render anything.
Also you do not need to maintain this value in state, instead use a ref
const SMSVerifyEnterPinScreen = ({ route, navigation }) => {
const isMounted = useRef(true)
const { phoneNumber } = route.params
useEffect(() => {
if(navigation) {
signInWithPhoneNumber(phoneNumber)
}
return () => {isMounted.current = false;}
}, [])
async function signInWithPhoneNumber(phoneNumber) {
const confirmation = await auth().signInWithPhoneNumber('+1'+phoneNumber)
...
}
return (
...
)
}

redux-thunk: actions are not dispatching

I am trying to build an app in react native that is suppose to take take two inputs by a user and then make a query to an api and get information about the two inputs. I have been having trouble with redux and redux-thunk and specifically with async actions.
This is the code in my app that i am specifically having trouble with
export const fetchData = url => {
console.log("start Fetching");
return async dispatch => { // this is where the problem is
dispatch(fetchingRequest());
try {
const response = await fetch("https://randomuser.me/api/?results=10");
const json = await response.text();
if (response.ok) {
dispatch(fetchingSuccess(json));
console.log("JSON", json);
} else {
console.log("fetch did not resolve");
}
} catch (error) {
dispatch(fetchingFailure(error));
}
};
console.log("Fetched data");
};
Upon debugging the function, I have ended with finding that when the fetchData function is called the function will execute but the async dispatch that is being returned has undefined behavior.
The output in the debugger when the function is called should be
start Fetching
JSON file information/Error
but the output in the debugger is actually
start Fetching
This is the function in which fetchData is called in
_onPress = () => {
let url = "https://randomuser.me/api/?results=10";
fetchData(url);
console.log("should have fetched");
};
this is the mapDispatchToProps function that I have added. The problem is i do not know what to add inside the function.
const mapStatetoDispatch = (url, dispatch) => {
return {dispatch(fetchData(url))}; // do not know what to place in body of function
};
i have connected it in the component with
export default connect(
mapStateToProps,
mapDispatchToProps
)(App);
these are the action creators that I import, if needed
import {
fetchingSuccess,
fetchingRequest,
fetchingFailure,
fetchData
} from "../data/redux/actions/appActions.js";
Assuming you have added redux-thunk as a middleware, it looks like the errors are here:
_onPress = () => {
const { fetchData } = this.props;
let url = "https://randomuser.me/api/?results=10";
fetchData(url);
console.log("should have fetched");
};
and
const mapStatetoDispatch = dispatch => ({
fetchData: url => dispatch(fetchData(url)),
}};