I have a simple application, and I want that on the first launch it should open a setup screen. After the user has finished the setup, and pressed the button then the values are stored with AsyncStorage. Then the React Navigation should react to this and push the user to the normal flow (Home screen). I have done everything, but my problem is that the user is not automatically pushed to the Home screen. The user has to restart the application in order to continue. This is my code:
App.js
const Stack = createStackNavigator();
function App() {
const [isSet, setIsSet] = useState(true);
async function checkSetup() {
const myValue = await AsyncStorage.getItem('#myValue');
if (myValue === null) {
setIsSet(false);
} else {
setIsSet(true);
}
}
useEffect(() => {
checkSetup();
}, []);
return (
<View style={{flex: 1, backgroundColor: '#272D2E'}}>
<NavigationContainer>
<Stack.Navigator screenOptions={{headerShown: false}}>
{isSet ? (
<>
<Stack.Screen name="Start" component={Start} />
<Stack.Screen name="Screen2" component={DocumentScan} />
<Stack.Screen name="Screen3" component={MailPhone} />
</>
) : (
<>
<Stack.Screen name="Setup" component={Setup} />
</>
)}
</Stack.Navigator>
</NavigationContainer>
</View>
);
}
export default App;
And in my Setup.js
function Setup({}) {
const navigation = useNavigation();
const [myValue, setMyValue] = useState('');;
const setStorage = async (name, value) => {
try {
await AsyncStorage.setItem(name, value);
} catch (error) {
console.log(error);
}
};
const goToNextStep = useCallback(async () => {
await setStorage('#myValue', myValue);
}, [myValue);
The value myValue is set from a TextInput. Does anyone know how to fix this?
Because checkSetup is not a hook and React does not track states of AsyncStorage.
To make AsyncStorage value as a React state, you can use custom hooks like the following:
const useAsyncStorage = (key) => {
const [value, setValue] = useState(null);
const set = useCallback(async (val) => {
await AsyncStorage.setItem(key, val);
setValue(val);
}, []);
const load = useCallback(async () => {
setValue(await AsyncStorage.getItem(key));
}, [key]);
useEffect(() => {
load();
}, [load]);
return [
value,
set
];
};
Then you can use this hook in your components:
App.js
const [myValue] = useAsyncStorage('#myValue');
useEffect(() => {
setIsSet(myValue !== null);
}, [myValue]);
Setup.js
const [, setMyValue] = useAsyncStorage('#myValue');
const goToNextStep = useCallback(async () => {
setMyValue(myValue);
}, [myValue, setMyValue]);
After set storage you can write following snippet.
history.pushState(state, title, url)
Related
I am trying to check if a user is authenticated, then navigate them to the Homepage, and if not navigated, direct them to the Pin Screen. But I have tried and I am getting this error
The action 'NAVIGATE' with payload {"name":"BottomBar","params":>{"screen":"Home"}} was not handled by any navigator.
Do you have a screen named 'BottomBar'?
If you're trying to navigate to a screen in a nested navigator.
Here is how I am trying to check the authentication
const App = () => {
const [onboarded, setOnboarded] = useState(false);
const [isBiometricSupported, setIsBiometricSupported] = useState(false);
const [isAuthenticated, setIsAuthenticated] = useState(false);
useEffect(() => {
(async () => {
const compatible = await LocalAuthentication.hasHardwareAsync();
setIsBiometricSupported(compatible);
})();
});
function onAuthenticate() {
const auth = LocalAuthentication.authenticateAsync({
promptMessage: 'Authenticate',
fallbackLabel: 'Enter Password',
});
auth.then(result => {
setIsAuthenticated(result.success);
console.log(result);
}
);
}
useEffect(() => {
getStorage();
}, []);
const getStorage = async () => {
const onboarded = await AsyncStorage.getItem('#onboarded');
setOnboarded(JSON.parse(onboarded));
};
const [loaded] = useFonts({
SFBold: require("./assets/fonts/sf-bold.ttf"),
SFMedium: require("./assets/fonts/sf-medium.ttf"),
SFRegular: require("./assets/fonts/sf-regular.ttf"),
SFLight: require("./assets/fonts/sf-light.ttf"),
});
if (!loaded) return null;
return (
<NavigationContainer theme={theme} >
<Stack.Navigator initialRouteName={onboarded ? 'BottomBar' : 'Onboarding'} screenOptions={{ headerShown: false }}>
<Stack.Screen name="BottomBar" component={BottomBar} />
<Stack.Screen name="PinScreen" component={PinScreen} />
<Stack.Screen name="Onboarding" component={Onboarding} />
<Stack.Screen name="SignUp" component={SignUp} />
<Stack.Screen name="SignIn" component={SignIn} />
</Stack.Navigator>
</NavigationContainer>
);
}
And here is my PinScreen, where the user should be navigated to the Homescreen after inserting the correct Pin
const navigation = useNavigation();
const [pin, setPin] = useState('');
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isPinSet, setIsPinSet] = useState(false);
const handlePinSubmit = async () => {
if (!isPinSet) {
try {
await AsyncStorage.setItem('#pin', pin);
setIsPinSet(true);
} catch (error) {
console.error(error);
}
} else {
try {
const storedPin = await AsyncStorage.getItem('#pin');
if (pin === storedPin) {
setIsAuthenticated(true);
navigation.navigate('BottomBar', { screen: 'Home' });
} else {
console.log('Incorrect PIN');
}
} catch (error) {
console.error(error);
}
}
};
Kindly Help
Try to avoind using useNavigation in components that act as screen. They per default receive a prop called navigation, which you may want to use to navigate, in fact.
Docs explanation
why you should not use useNavigation hook inside screens
So try to change your useNavigation hook (dont use it) and use navigation prop that your pin screen receives.
I have a React component using an infinite scroll to fetch information from an api using a pageToken.
When the user hits the bottom of the page, it should fetch the next bit of information. I thought myself clever for passing the pageToken to a useEffect hook, then updating it in the hook, but this is causing all of the api calls to run up front, thus defeating the use of the infinite scroll.
I think this might be related to React's derived state, but I am at a loss about how to solve this.
here is my component that renders the dogs:
export const Drawer = ({
onClose,
}: DrawerProps) => {
const [currentPageToken, setCurrentPageToken] = useState<
string | undefined | null
>(null);
const {
error,
isLoading,
data: allDogs,
nextPageToken,
} = useDogsList({
pageToken: currentPageToken,
});
const loader = useRef(null);
// When user scrolls to the end of the drawer, fetch more dogs
const handleObserver = useCallback(
(entries) => {
const [target] = entries;
if (target.isIntersecting) {
setCurrentPageToken(nextPageToken);
}
},
[nextPageToken],
);
useEffect(() => {
const option = {
root: null,
rootMargin: '20px',
threshold: 0,
};
const observer = new IntersectionObserver(handleObserver, option);
if (loader.current) observer.observe(loader.current);
}, [handleObserver]);
return (
<Drawer
onClose={onClose}
>
<List>
{allDogs?.map((dog) => (
<Fragment key={dog?.adopterAttributes?.id}>
<ListItem className={classes.listItem}>
{dog?.adopterAttributes?.id}
</ListItem>
</Fragment>
))}
{isLoading && <div>Loading...</div>}
<div ref={loader} />
</List>
</Drawer>
);
};
useDogsList essentially looks like this with all the cruft taken out:
import { useEffect, useRef, useState } from 'react';
export const useDogsList = ({
pageToken
}: useDogsListOptions) => {
const [isLoading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [nextPageToken, setNextPageToken] = useState<string | null | undefined>(
null,
);
const [allDogs, setAllDogs] = useState(null);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const result =
await myClient.listDogs(
getDogsRequest,
{
token,
},
);
const dogListObject = result?.toObject();
const newDogs = result?.dogsList;
setNextPageToken(dogListObject?.pagination?.nextPageToken);
// if API returns a pageToken, that means there are more dogs to add to the list
if (nextPageToken) {
setAllDogs((previousDogList) => [
...(previousDogList ?? []),
...newDogs,
]);
}
}
} catch (responseError: unknown) {
if (responseError instanceof Error) {
setError(responseError);
} else {
throw responseError;
}
} finally {
setLoading(false);
}
};
fetchData();
}, [ pageToken, nextPageToken]);
return {
data: allDogs,
nextPageToken,
error,
isLoading,
};
};
Basically, the api call returns the nextPageToken, which I want to use for the next call when the user hits the intersecting point, but because nextPageToken is in the dependency array for the hook, the hook just keeps running. It retrieves all of the data until it compiles the whole list, without the user scrolling.
I'm wondering if I should be using useCallback or look more into derivedStateFromProps but I can't figure out how to make this a "controlled" component. Does anyone have any guidance here?
I suggest a small refactor of the useDogsList hook to instead return a hasNext flag and fetchNext callback.
export const useDogsList = ({ pageToken }: useDogsListOptions) => {
const [isLoading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [nextPageToken, setNextPageToken] = useState<string | null | undefined>(
pageToken // <-- initial token value for request
);
const [allDogs, setAllDogs] = useState([]);
// memoize fetchData callback for stable reference
const fetchData = useCallback(async () => {
setLoading(true);
try {
const result = await myClient.listDogs(getDogsRequest, { token: nextPageToken });
const dogListObject = result?.toObject();
const newDogs = result?.dogsList;
setNextPageToken(dogListObject?.pagination?.nextPageToken ?? null);
setAllDogs((previousDogList) => [...previousDogList, ...newDogs]);
} catch (responseError) {
if (responseError instanceof Error) {
setError(responseError);
} else {
throw responseError;
}
} finally {
setLoading(false);
}
}, [nextPageToken]);
useEffect(() => {
fetchData();
}, []); // call once on component mount
return {
data: allDogs,
hasNext: !!nextPageToken, // true if there is a next token
error,
isLoading,
fetchNext: fetchData, // callback to fetch next "page" of data
};
};
Usage:
export const Drawer = ({ onClose }: DrawerProps) => {
const { error, isLoading, data: allDogs, hasNext, fetchNext } = useDogsList({
pageToken // <-- pass initial page token
});
const loader = useRef(null);
// When user scrolls to the end of the drawer, fetch more dogs
const handleObserver = useCallback(
(entries) => {
const [target] = entries;
if (target.isIntersecting && hasNext) {
fetchNext(); // <-- Only fetch next if there is more to fetch
}
},
[hasNext, fetchNext]
);
useEffect(() => {
const option = {
root: null,
rootMargin: "20px",
threshold: 0
};
const observer = new IntersectionObserver(handleObserver, option);
if (loader.current) observer.observe(loader.current);
// From #stonerose036
// clear previous observer in returned useEffect cleanup function
return observer.disconnect;
}, [handleObserver]);
return (
<Drawer onClose={onClose}>
<List>
{allDogs?.map((dog) => (
<Fragment key={dog?.adopterAttributes?.id}>
<ListItem className={classes.listItem}>
{dog?.adopterAttributes?.id}
</ListItem>
</Fragment>
))}
{isLoading && <div>Loading...</div>}
<div ref={loader} />
</List>
</Drawer>
);
};
Disclaimer
Code hasn't been tested, but IMHO it should be pretty close to what you are after. There may be some minor tweaks necessary to satisfy any TSLinting issues, and getting the correct initial page token to the hook.
While Drew and #debuchet's answers helped me improve the code, the problem around multiple renders ended up being solved by tackling the observer itself. I had to disconnect it afterwards
useEffect(() => {
const option = {
root: null,
rootMargin: '20px',
threshold: 0,
};
const observer = new IntersectionObserver(handleObserver, option);
if (loader.current) observer.observe(loader.current);
return () => {
observer.disconnect();
};
}, [handleObserver]);
I want to retrieve a field value of a document in Users collection by referencing it via the where condition from Firestore. I use the context api to pass the user object of the logged in user in my app. I get this error that user.uid is null. I can't spot where the mistake is. I have added the relevant piece of code.
EditProfile.js
const EditProfile = () => {
const { user } = React.useContext(AuthContext);
const [name, setName] = React.useState();
React.useEffect(() => {
const userid = user.uid;
const name = getFieldValue("Users", userid);
setName(name);
}, []);
};
export default EditProfile;
passing and getting value via context
export const AuthContext = React.createContext();
export const AuthProvider = ({ children }) => {
const [user, setUser] = React.useState(null);
return (
<AuthContext.Provider
value={{
user,
setUser,
}}
>
{children}
</AuthContext.Provider>
);
};
const AppStack = () => {
return (
<AuthProvider>
<BottomTab.Navigator>
<BottomTab.Screen
name="ProfileStack"
component={ProfileStack}
/>
</BottomTab.Navigator>
</AuthProvider>
);
};
export default AppStack;
ProfileStack.js
export const ProfileStack = ({ navigation }) => {
return (
<Stack.Navigator>
<Stack.Screen
name="Profile"
component={Profile}
/>
<Stack.Screen
name="EditProfile"
component={EditProfile}
/>
</Stack.Navigator>
);
};
getFieldValue function
export const getFieldValue = (collection, userid) => {
firestore()
.collection(collection)
.where("userid", "==", userid)
.get()
.then((querySnapshot) => {
if (querySnapshot.size === 0) {
return "";
}
if (querySnapshot.size === 1) {
const { name } = querySnapshot[0].data();
return name;
}
})
.catch((e) => console.log(e));
};
Routing file
const Routes = () => {
// Set an initializing state whilst Firebase connects
const [initializing, setInitializing] = React.useState(true);
const { user, setUser } = React.useContext(AuthContext);
// Handle user state changes
const onAuthStateChanged = (user) => {
setUser(user);
if (initializing) setInitializing(false);
};
React.useEffect(() => {
RNBootSplash.hide();
const subscriber = auth().onAuthStateChanged(onAuthStateChanged);
return subscriber; // unsubscribe on unmount
}, []);
if (initializing) return null;
return (
<NavigationContainer>
{user ? <AppStack /> : <AuthStack />}
</NavigationContainer>
);
};
export default Routes;
I am new to React Native and don't quite understand the concept of initial states of an object and updating the state when I have more than one property to set.
the error (edit #2):
Objects are not valid as a React child (found: object with keys {userRole}). If you meant to render a collection of children, use an array instead.
App.js
const initialLoginState = {
userRole: null,
userId: null,
};
const [user, setUser] = useState(initialLoginState);
const [isReady, setIsReady] = useState(false);
const restoreUser = async () => {
const user = await authStorage.getUser();
if (user) setUser(user);
};
if (!isReady) {
return (
<AppLoading
startAsync={restoreUser}
onFinish={() => setIsReady(true)}
onError={console.warn}
/>
);
}
//render
return (
<AuthContext.Provider value={{ user, setUser }}>
<NavigationContainer>
{user.userRole ? <ViewTest /> : <AuthNavigator />}
</NavigationContainer>
</AuthContext.Provider>
);
useAuth which updates the user when I received the data:
const logIn = (data, authToken) => {
setUser((prevState) => ({
userRole: {
...prevState.userId,
userRole: data.USERROLE,
},
}));
authStorage.storeToken(data.USERID);
};
You don't need prevState in functional component. user is the prevState before you set new state
const logIn = (data, authToken) => {
setUser({...user, userRole: data.USERROLE});
authStorage.storeToken(data.USERID);
};
Objects are not valid as a React child (found: object with keys {userRole}). If you meant to render a collection of children, use an array instead.
<AuthContext.Provider value={{ user, setUser }}> // <---- the problem is here
<NavigationContainer>
{user.userRole ? <ViewTest /> : <AuthNavigator />}
</NavigationContainer>
</AuthContext.Provider>
I'm not sure what AuthContext.Provider is, but it's trying to render the object(User) as html react elements, make sure you know what sort of data the value prop of that component takes.
I was able to get the right answer with the help of #P.hunter, #Erdenezaya and #Federkun.
The problem was in the state init and setUser().
App.js
const initialLoginState = {
userRole: null,
userId: null,
};
const [user, setUser] = useState({
initialLoginState,
});
const [isReady, setIsReady] = useState(false);
const restoreUser = async () => {
const user = await authStorage.getUser();
if (user) setUser(user);
};
if (!isReady) {
return (
<AppLoading
startAsync={restoreUser}
onFinish={() => setIsReady(true)}
onError={console.warn}
/>
);
}
//syntax error was found in {user.userRole}
return (
<AuthContext.Provider value={{ user, setUser }}>
<NavigationContainer>
{user.userRole ? <ViewTest /> : <AuthNavigator />}
</NavigationContainer>
</AuthContext.Provider>
);
Context functionality for setting the user had to be done like this:
export default useAuth = () => {
const { user, setUser } = useContext(AuthContext);
const logIn = (data, authToken) => {
setUser({ ...user, userRole: data.USERROLE });
authStorage.storeToken(data.USERID);
};
const logOut = () => {
setUser({ ...user, userRole: null });
authStorage.removeToken();
};
return { user, logIn, logOut };
};
Thank you all for your help!
I am trying to get an array of objects from my Redux-Store state called user and save it to async storage and use useState with the response to set the state before I retrieve it and view it with the FlatList however I am getting an error along the lines of Warning: Can't perform a React state update on an unmounted component. The user details is being set to the redux store in another component and then being retrieved from the current component I am displaying. Please could I get your help . I would really appreciate it. Thank you in advance!!!
const TheUser = (props) => {
//user is an array from redux store
const user = useSelector(state => state.account.cookbook)
const [getUser, setGetUser] = useState()
const saveUserAsync = async () => {
await AsyncStorage.setItem('user', JSON.stringify(user))
}
saveUserAsync()
AsyncStorage.getItem('user').then(response => {
setGetUser(response)
})
return (
<FlatList
data={getUser}
keyExtractor={item => item.id}
renderItem={itemData =>
<MyUser
name={itemData.item.name}
image={itemData.item.imageUri}
details={itemData.item.details.val}
/>
}
/>
)
}
export default TheUser
You can use useEffect hook to solve this problem.
IS_MOUNTED variable will track if component is mounted or not.
let IS_MOUNTED = false; // global value
const TheUser = (props) => {
//user is an array from redux store
const user = useSelector(state => state.account.cookbook)
const [getUser, setGetUser] = useState()
const saveUserAsync = async () => {
await AsyncStorage.setItem('user', JSON.stringify(user))
}
AsyncStorage.getItem('user').then(response => {
if(IS_MOUNTED)
{
setGetUser(JSON.parse(response));
}
});
useEffect(() => {
IS_MOUNTED = true;
saveUserAsync();
return (() => {
IS_MOUNTED = false;
})
},[])
return (
<FlatList
data={getUser}
keyExtractor={item => item.id}
renderItem={itemData =>
<MyUser
name={itemData.item.name}
image={itemData.item.imageUri}
details={itemData.item.details.val}
/>
}
/>
)
}
export default TheUser
import { useEffect } from "react"
let isMount = true
const TheUser = (props) => {
//user is an array from redux store
const user = useSelector(state => state.account.cookbook)
// const [getUser, setGetUser] = useState()
// useEffect(() => {
// const saveUserAsync = async () => {
// await AsyncStorage.setItem('user', JSON.stringify(user))
// const response = await AsyncStorage.getItem('user')
// if (isMount)
// setGetUser(JSON.parse(response))
// }
// saveUserAsync()
// }, [user])
// useEffect(() => {
// isMount = true
// return () => {
// isMount = false
// }
// }, [])
return (
<FlatList
// data={getUser}
data={user}
keyExtractor={item => item.id}
renderItem={itemData =>
<MyUser
name={itemData.item.name}
image={itemData.item.imageUri}
details={itemData.item.details.val}
/>
}
/>
)
}
export default TheUser