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!
Related
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 want to display the Stripe subscription form after user has signed up into the extension. I want to display home to a subscribed user. And whenever the user opens the extension the home should be displayed if he has already subscribed. If not, it should display subscription form.
But the problem is my app is displaying both the home and subscription form to a subscribed user.
Here is my private route code:
const PrivateRoute = ({ component: RouteComponent, ...rest }) => {
const { currentUser, subscriptionStatus } = useContext(AuthContext);
return (
<Route
{...rest}
render={(routeProps) =>
!!currentUser ? (
!!subscriptionStatus ? (
<RouteComponent {...routeProps} />
)
: (
<Redirect to={"/subscribe"} />
)
) : (
<Redirect to={"/login"} />
)
}
/>
);
};
This is my auth context provider:
export const AuthProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState(null);
const [pending, setPending] = useState(true);
const [emailVerified, setEmailVerified] = useState(true);
const [helper, setHelper] = useState(false);
const [subscriptionStatus, setSubscriptionStatus] = useState(null);
useEffect(() => {
app.auth().onAuthStateChanged(async(user) => {
setCurrentUser(user);
if(!user.emailVerified){
setEmailVerified(false);
}else{
setEmailVerified(true);
const fetchData = async () => {
const token = user && (await user.getIdToken());
const payloadHeader = {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
};
const status = await fetch('http://localhost:3000/is-subscribed', payloadHeader).then(r => r.json());
if(status == 'active'){
setSubscriptionStatus(status);
setPending(false);
}else{
setPending(false);
}
}
fetchData();
}
});
}, []);
if (pending && helper) {
return <Loader />;
}
if(!emailVerified){
return <>Please verify your email</>
}
return (
<AuthContext.Provider
value={{
currentUser, subscriptionStatus
}}
>
{children}
</AuthContext.Provider>
);
};
Any idea on this?
The easiest option would be redirecting your customers to Checkout to pay, and handling the successful payments in your Firebase app via webhooks, but you can also use the custom flow if you prefer.
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)
I store three values in the context provider wrapper function. I need to update the context states in the login and logout components, and use them in navbar and in any other places.
const AppContext = createContext();
const AppContextProvider = (props) => {
const loggedIn = Cookies.get('isLoggedIn') ? true : false;
const [ isLoggedIn, setIsLoggedIn ] = useState(isLoggedIn);
const [ userID, setUserID ] = useState('');
const [ email, setEmail ] = useState('');
return (
<AppContext.Provider value={[isLoggedIn, setIsLoggedIn, userID, setUserID, email, setEmail ]}>
{props.children}
</AppContext.Provider>
);
}
export { AppContext, AppContextProvider };
In the _app.js it's this:
function MyApp({ Component, pageProps }) {
return (
<>
<AppContextProvider>
<NavBar />
<Component {...pageProps} />
<Footer />
</AppContextProvider>
</>
);
}
But now, the trouble is, I can't understand how to update these context states. This is my use case in login component:
const [ isLoggedIn, setIsLoggedIn, userID, setUserID, email, setEmail ] = useContext(AppContext);
const handleSubmit = async() => {
const url = process.env.NEXT_PUBLIC_URL + 'auth/login/';
const data = { "email": email, "password": password };
try {
const resp = await axios.post(url, data);
const obj = await resp.data;
Cookies.set('isLoggedIn', true, { secure: true }, { sameSite: 'lax' });
setIsLoggedIn(true);
setUserID(obj.uid);
setEmail(obj.email);
} catch (err) {...};
This way, it didn't work. I could not get the context values in other components. How do I update multiple context states?
EDIT:
Following #Drew Reese's answer, this is my working Context file:
import Cookies from 'js-cookie';
import { useState, createContext } from 'react';
const AppContext = createContext({
email:'',
isLoggedIn: false,
userID: '',
setEmail: () => {},
setIsLoggedIn: () => {},
setUserID: () => {},
});
const AppContextProvider = (props) => {
const [ email, setEmail ] = useState(Cookies.get('email') || null);
const [ userID, setUserID ] = useState(Cookies.get('uid') || null);
const [ isLoggedIn, setIsLoggedIn ] = useState(Cookies.get('isLoggedIn') ? true : false);
return (
<AppContext.Provider value={{ email, setEmail, isLoggedIn, setIsLoggedIn, userID, setUserID }}>
{props.children}
</AppContext.Provider>
);
}
export { AppContext, AppContextProvider };
Your default context value should match what consumers expect.
Updating Context from a Nested Component
It is often necessary to update the context from a component that is
nested somewhere deeply in the component tree. In this case you can
pass a function down through the context to allow consumers to update
the context:
// Make sure the shape of the default value passed to
// createContext matches the shape that the consumers expect!
export const ThemeContext = React.createContext({
theme: themes.dark,
toggleTheme: () => {},
});
Update your AppContext default value to match what consumers will be using.
const AppContext = createContext([
false, // isLoggedIn
() => {}, // setIsLoggedIn
'', // userID
() => {}, // setUserID
'', // email
() => {}, // setEmail
]);
Using an array like this may be a little cumbersome, consumers would need to keep the array indices straight when using destructuring assignment. Using an object instead makes consuming the context value a little more wieldy, now the order is irrelevant.
const AppContext = createContext({
email: '',
isLoggedIn: false,
userID: '',
setEmail: () => {},
setIsLoggedIn: () => {},
setUserID: () => {},
});
Usage:
const {
isLoggedIn,
setIsLoggedIn,
userID,
setUserID,
email,
setEmail,
} = useContext(AppContext);
I could not get the context values in other components. How do I
update multiple context states?
You should ensure that all consumers that you want to be able to update the context value are actually nested in the same AppContextProvider component.
I was working on an old app using react navigation version 4 the app contains a register and login in page obviously and then the content of the app.
recently I started remaking the content of the app using react navigation version 5 in order to use the shared element animation and the bottom tab navigator and it was fairly simple.
but I struggled with converting the login part to version 5 since the app structure is somewhat complicated and I am somewhat new to react navigation version 5.
i will leave a figure of the app structure bellow a long with samples of the code used.
App.js :
import { setNavigator } from "./app/navigationRef";
const articleListFlow = createStackNavigator({
Main: MainScreen, // screen with diffrent articles categories
ResultsShow: ResultShowScreen, // article details screen
});
const loginFlow = createStackNavigator({
Signup: SignupScreen,
Signin: SigninScreen,
});
loginFlow.navigationOptions = () => {
return {
headerShown: false,
};
};
articleListFlow.navigationOptions = {
title: "News Feed",
tabBarIcon: ({ tintColor }) => (
<View>
<Icon style={[{ color: tintColor }]} size={25} name={"ios-cart"} />
</View>
),
activeColor: "#ffffff",
inactiveColor: "#ebaabd",
barStyle: { backgroundColor: "#d13560" },
};
const switchNavigator = createSwitchNavigator({
ResolveAuth: ResolveAuthScreen,
MainloginFlow: createSwitchNavigator({
//WelcomeScreen: WeclomeScreen,
loginFlow: loginFlow,
}),
mainFlow: createMaterialBottomTabNavigator(
{
articleListFlow: articleListFlow,
ArticleSave: ArticleSaveScreen, // we dont need this one
Account: AccountScreen,
},
{
activeColor: "#ffffff",
inactiveColor: "#bda1f7",
barStyle: { backgroundColor: "#6948f4" },
}
),
});
const App = createAppContainer(switchNavigator);
export default () => {
return (
<AuthProvider>
<App
ref={(navigator) => {
setNavigator(navigator);
}}
/>
</AuthProvider>
);
};
NavigationRef.js :
import { NavigationActions } from "react-navigation";
let navigator;
export const setNavigator = (nav) => {
navigator = nav;
};
export const navigate = (routeName, params) => {
navigator.dispatch(
NavigationActions.navigate({
routeName,
params,
})
);
};
// routename is the name of the routes singin singup accountscreen
// params information we want to pass to the screen we want to show
AuthContext.js
import { AsyncStorage } from "react-native";
import createDataContext from "./createDataContext";
import userAPI from "../api/user";
// using navigate to access the navigator and redirect the user
import { navigate } from "../navigationRef";
// AUTHENTICATION REDUCERS
const authReducer = (state, action) => {
switch (action.type) {
case "add_error": {
return {
...state,
errorMessage: action.payload,
};
}
case "clear_error_message": {
return {
...state,
errorMessage: "",
};
}
case "signin": {
return {
errorMessage: "",
token: action.payload,
};
}
default:
return state;
}
};
// CLEARING ERROR MESSAGES WHEN SWITCHING SIGNIN-SIGNUP
const clearErrorMessage = (dispatch) => () => {
dispatch({ type: "clear_error_message" });
};
// AUTOMATIC SIGNIN ONLY USING TOKENS ON USER DEVICE
const tryLocalSignin = (dispatch) => async () => {
const token = await AsyncStorage.getItem("token");
if (token) {
// if token exists
dispatch({ type: "signin", payload: token });
navigate("Main");
} else {
// if token doesnt exist
navigate("WelcomeScreen");
}
};
// SIGNUP
const signup = (dispatch) => async ({ email, password }) => {
try {
const response = await userAPI.post("/signup", { email, password });
await AsyncStorage.setItem("token", response.data.token);
dispatch({ type: "signin", payload: response.data.token });
// making use of the navigate component to access navigation
// and redirect the user
navigate("Main");
} catch (err) {
dispatch({
type: "add_error",
payload: "Something went wrong with sign up",
});
}
};
// SIGNIN
const signin = (dispatch) => async ({ email, password }) => {
try {
const response = await userAPI.post("/signin", { email, password });
await AsyncStorage.setItem("token", response.data.token);
// using signin since the logic is the same
dispatch({ type: "signin", payload: response.data.token });
// making use of the navigate component to access navigation
// and redirect the user
navigate("Main");
} catch (err) {
console.log(err);
dispatch({
type: "add_error",
payload: "Something went wrong with sign in",
});
}
};
// SIGNOUT
const signout = (dispatch) => async () => {
// removing the token makes identification not work again
await AsyncStorage.removeItem("token");
dispatch({ type: "signout" });
navigate("loginFlow");
};
// CREATING CONTEXT AND PROVIDER OBJECTS FOR AUTHENTICATION
export const { Provider, Context } = createDataContext(
authReducer,
{
signin,
signup,
signout,
clearErrorMessage,
tryLocalSignin,
},
{
token: null,
errorMessage: "",
}
);
createDataContext.js
import React, { useReducer } from "react";
export default (reducer, actions, defaultValue) => {
const Context = React.createContext();
const Provider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, defaultValue);
const boundActions = {};
for (let action in actions) {
// for every action in the actions, call it with dispatch
boundActions[action] = actions[action](dispatch);
}
return (
<Context.Provider value={{ state, ...boundActions }}>
{children}
</Context.Provider>
);
};
return { Context, Provider };
};
My appologies for the long code and thank you in advance for anyone who can help.
There are several things that you need to consider when moving from V4 to V5 it involves some changes and also you can consider using features like the hooks.
The first change will be removing the Switch Navigator and conditionally render the navigator in its place. This will be done in your App.js. As you already have a reducer based implementation you can use the state values to take this decision.
The next change will be the creation of stacks, in V4 you create the navigation by passing the screen, now everything is a component and you pass the screens as children.
The option are also sent as props to either the navigator or the screen itself.
The usage of navigation ref is still possible but you can also use hooks like usenavigation inside components and for your authentication flow you wont be using this as you conditionally render the navigators.
I have made a simplified version based on your code.
App.js
const AuthStack = createStackNavigator();
const AppTabs = createMaterialBottomTabNavigator();
const ArticleStack = createStackNavigator();
const Articles = () => {
return (
<ArticleStack.Navigator>
<AppTabs.Screen name="ArticlesList" component={ArticleList} />
<AppTabs.Screen name="ArticlesDetails" component={ArticleDetail} />
</ArticleStack.Navigator>
);
};
export default function App() {
const [state, dispatch] = React.useReducer(authReducer, {
isLoading: true,
token: null,
errorMessage: '',
});
React.useEffect(() => {
const bootstrapAsync = async () => {
const userToken = await AsyncStorage.getItem('userToken');
dispatch({ type: 'RESTORE_TOKEN', token: userToken });
};
bootstrapAsync();
}, []);
const authContext = React.useMemo(
() => ({
signIn: async (data) => {
dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
},
signOut: () => dispatch({ type: 'SIGN_OUT' }),
signUp: async (data) => {
dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
},
}),
[]
);
return (
<AuthContext.Provider value={authContext}>
<NavigationContainer>
{state.token === null ? (
<AuthStack.Navigator headerMode="none">
{state.isLoading ? (
<AuthStack.Screen name="Welcome" component={WelcomeScreen} />
) : (
<>
<AuthStack.Screen name="SignIn" component={SignInScreen} />
<AuthStack.Screen name="SignUp" component={SingUpScreen} />
</>
)}
</AuthStack.Navigator>
) : (
<AppTabs.Navigator
activeColor="#f0edf6"
inactiveColor="#3e2465"
barStyle={{ backgroundColor: '#694fad' }}>
<AppTabs.Screen
name="Articles"
component={Articles}
options={{
tabBarLabel: 'Home',
tabBarIcon: ({ color, size }) => (
<MaterialCommunityIcons
name="home"
color={color}
size={size}
/>
),
}}
/>
<AppTabs.Screen name="Search" component={SearchScreen} />
<AppTabs.Screen name="Save" component={SaveScreen} />
<AppTabs.Screen name="Account" component={AccountScreen} />
</AppTabs.Navigator>
)}
</NavigationContainer>
</AuthContext.Provider>
);
}
Auth Context
const AuthContext = React.createContext();
export default AuthContext;
Auth Reducer
export const authReducer = (state, action) => {
switch (action.type) {
case 'RESTORE_TOKEN':
return {
...state,
token: action.token,
isLoading: false,
};
case 'SIGN_IN': {
return {
errorMessage: '',
token: action.payload,
};
}
case 'SIGN_OUT': {
return {
errorMessage: '',
token: null,
};
}
default:
return state;
}
};
As you can see the flow will be showing the welcome screen till the token is loaded from async storage and then based on that show the tabs or the login screen. Also the parameters are passed as props. I've moved the actions to app.js but it can be separated as well.
You can see a fully running sample here
https://snack.expo.io/#guruparan/navigation-sample-3
Hope this helps, Feel free to ask if there are any questions.
As per your diagram, I have tried to create Navigation
const WelcomeStack = createStackNavigator();
const Tab = createBottomTabNavigator();
const ArticleStack = createStackNavigator();
const MainStack = createStackNavigator();
function Welcome(){
return(
<WelcomeStack.Navigator>
<WelcomeStack.screen name='SignIn' component={SignIn}/>
<WelcomeStack.screen name='SignUp' component={SignUp}/>
</WelcomeStack.Navigator>
)
}
function Article(){
return(
<ArticleStack.Navigator>
<ArticleStack.Screen name='ArtcileList' name={ArticleList}/>
<ArticleStack.Screen name='ArticleDetail' name={ArtcileDetail}/>
</ArticleStack.Navigator>
)
}
function TabNav(){
<Tab.Navigator>
<Tab.Screen name='Article' component={Article}/>
<Tab.Screen name='Search' component={Search}/>
<Tab.Screen name='Save' component={Save}/>
<Tab.Screen name='Account' component={Account}/>
</Tab.Navigator>
}
function App(){
return(
<NavigationContainer>
<MainStack.Navigator>
{this.state.isLogin ?
<MainStack.Screen name='Tab' component={TabNav}/>
:
<MainStack.Screen name = 'WelcomeStack' component={Welcome}/>
}
</MainStack.Navigator>
</NavigationContainer>
)
}
In react navigation 5, their is no switch navigator so you have to go with stack navigation + ternary operator.
This is just an idea as per your diagram. You can make it better after some R&D.