When I learn authentication flow of react navigation 6.0, I read the sample code which used redux, it used dummy token, but In my react native project, I have to get the real token from server, So I tried to add some code in the sample project.
import * as React from 'react';
import { Button, Text, TextInput, View } from 'react-native';
import { NavigationContainer } from '#react-navigation/native';
import { createStackNavigator } from '#react-navigation/stack';
import { useMutation, gql } from '#apollo/client';
import {
ApolloClient,
ApolloProvider,
createHttpLink,
InMemoryCache
} from '#apollo/client';
import { setContext } from 'apollo-link-context';
const SIGNIN_USER = gql`
mutation signIn($email: String!, $password: String!) {
signIn(email: $email, password: $password)
}
`;
// for example, my server address
const API_URI='https://xxxx.herokuapp.com/api';
const cache = new InMemoryCache();
const client = new ApolloClient({
uri: API_URI,
cache: new InMemoryCache()
});
const AuthContext = React.createContext();
function SplashScreen() {
return (
<View>
<Text>Loading...</Text>
</View>
);
}
function HomeScreen() {
const { signOut } = React.useContext(AuthContext);
return (
<View>
<Text>Signed in!</Text>
<Button title="Sign out" onPress={signOut} />
</View>
);
}
function SignInScreen() {
const [username, setUsername] = React.useState('');
const [password, setPassword] = React.useState('');
const { signIn } = React.useContext(AuthContext);
return (
<View>
<TextInput
placeholder="Username"
value={username}
onChangeText={setUsername}
/>
<TextInput
placeholder="Password"
value={password}
onChangeText={setPassword}
secureTextEntry
/>
<Button title="Sign in" onPress={() => signIn({ username, password })} />
</View>
);
}
const Stack = createStackNavigator();
export default function App({ navigation }) {
const [state, dispatch] = React.useReducer(
(prevState, action) => {
switch (action.type) {
case 'RESTORE_TOKEN':
return {
...prevState,
userToken: action.token,
isLoading: false,
};
case 'SIGN_IN':
return {
...prevState,
isSignout: false,
userToken: action.token,
};
case 'SIGN_OUT':
return {
...prevState,
isSignout: true,
userToken: null,
};
}
},
{
isLoading: true,
isSignout: false,
userToken: null,
}
);
React.useEffect(() => {
// Fetch the token from storage then navigate to our appropriate place
const bootstrapAsync = async () => {
let userToken;
try {
// Restore token stored in `SecureStore` or any other encrypted storage
// userToken = await SecureStore.getItemAsync('userToken');
} catch (e) {
// Restoring token failed
}
// After restoring token, we may need to validate it in production apps
// This will switch to the App screen or Auth screen and this loading
// screen will be unmounted and thrown away.
dispatch({ type: 'RESTORE_TOKEN', token: userToken });
};
bootstrapAsync();
}, []);
const authContext = React.useMemo(
() => ({
signIn: async ({username, password}) => {
useMutation(SIGNIN_USER, {
variables:{
email: username,
password: password
},
onCompleted: data => {
console.log(data);
}
});
// In a production app, we need to send some data (usually username, password) to server and get a token
// We will also need to handle errors if sign in failed
// After getting token, we need to persist the token using `SecureStore` or any other encrypted storage
// In the example, we'll use a dummy token
dispatch({ type: 'SIGN_IN', token: data.signIn });
},
signOut: () => dispatch({ type: 'SIGN_OUT' }),
signUp: async (data) => {
// In a production app, we need to send user data to server and get a token
// We will also need to handle errors if sign up failed
// After getting token, we need to persist the token using `SecureStore` or any other encrypted storage
// In the example, we'll use a dummy token
dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
},
}),
[]
);
return (
<AuthContext.Provider value={authContext}>
<ApolloProvider client={client}>
<NavigationContainer>
<Stack.Navigator>
{state.isLoading ? (
// We haven't finished checking for the token yet
<Stack.Screen name="Splash" component={SplashScreen} />
) : state.userToken == null ? (
// No token found, user isn't signed in
<Stack.Screen
name="SignIn"
component={SignInScreen}
options={{
title: 'Sign in',
// When logging out, a pop animation feels intuitive
animationTypeForReplace: state.isSignout ? 'pop' : 'push',
}}
/>
) : (
// User is signed in
<Stack.Screen name="Home" component={HomeScreen} />
)}
</Stack.Navigator>
</NavigationContainer>
</ApolloProvider>
</AuthContext.Provider>
);
}
And I get the error,
WARN Possible Unhandled Promise Rejection (id: 0):
Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
I searched it, it is because I use hooks in useMemo.
https://reactjs.org/warnings/invalid-hook-call-warning.html
To avoid confusion, itβs not supported to call Hooks in other cases:
π΄ Do not call Hooks in class components.
π΄ Do not call in event handlers.
π΄ Do not call Hooks inside functions passed to useMemo, useReducer, or useEffect.
But, I have to send some data to server to get a token, and save token by Securestore or something else, If I can't write this section here, where?
I have no idea about where to write this section of code. My understanding to Redux in this situation:
when a user click button of 'sign in', call the function of signIn which is declared in authContext, useMemo, in use useMemo, get the token, save the token, then dispatch to notify data change, then go to the reducer.
After read a basic tutorial of redux, I have finished that by:
In SignInScreen, get the web token by 'login' function, which is triggered by the button of 'Sign in'
After execute it successfully, invoke the 'signIn' to update the state(by call dispatch).
I was confused with the name of 'signIn' function before, which is the same with my eventHandler function of 'signIn' button in SignInScreen. Actually, they are in duty of different logic.
import * as React from 'react';
import { Button, Text, TextInput, View } from 'react-native';
import { NavigationContainer } from '#react-navigation/native';
import { createStackNavigator } from '#react-navigation/stack';
import { useMutation, gql } from '#apollo/client';
import {
ApolloClient,
ApolloProvider,
createHttpLink,
InMemoryCache
} from '#apollo/client';
import { setContext } from 'apollo-link-context';
const SIGNIN_USER = gql`
mutation signIn($email: String!, $password: String!) {
signIn(email: $email, password: $password)
}
`;
const API_URI='https://jseverywhere.herokuapp.com/api';
const cache = new InMemoryCache();
const client = new ApolloClient({
uri: API_URI,
cache: new InMemoryCache()
});
const AuthContext = React.createContext();
function SplashScreen() {
return (
<View>
<Text>Loading...</Text>
</View>
);
}
function HomeScreen() {
const { signOut } = React.useContext(AuthContext);
return (
<View>
<Text>Signed in!</Text>
<Button title="Sign out" onPress={signOut} />
</View>
);
}
function SignInScreen() {
const [username, setUsername] = React.useState('');
const [password, setPassword] = React.useState('');
const { signIn } = React.useContext(AuthContext);
const [logIn, {loading, error}] = useMutation(SIGNIN_USER, {
variables:{
email: username,
password: password
},
onCompleted: data => {
console.log(data);
signIn(data);
}
});
if(loading)
return <Text>Loading...</Text>;
if(error)
return <Text>Error--{error.message}</Text>;
return (
<View>
<TextInput
placeholder="Username"
value={username}
onChangeText={setUsername}
/>
<TextInput
placeholder="Password"
value={password}
onChangeText={setPassword}
secureTextEntry
/>
<Button title="Sign in" onPress={() => logIn()} />
</View>
);
}
const Stack = createStackNavigator();
export default function App({ navigation }) {
const [state, dispatch] = React.useReducer(
(prevState, action) => {
switch (action.type) {
case 'RESTORE_TOKEN':
return {
...prevState,
userToken: action.token,
isLoading: false,
};
case 'SIGN_IN':
return {
...prevState,
isSignout: false,
userToken: action.token,
};
case 'SIGN_OUT':
return {
...prevState,
isSignout: true,
userToken: null,
};
}
},
{
isLoading: true,
isSignout: false,
userToken: null,
}
);
React.useEffect(() => {
// Fetch the token from storage then navigate to our appropriate place
const bootstrapAsync = async () => {
let userToken;
try {
// Restore token stored in `SecureStore` or any other encrypted storage
// userToken = await SecureStore.getItemAsync('userToken');
} catch (e) {
// Restoring token failed
}
// After restoring token, we may need to validate it in production apps
// This will switch to the App screen or Auth screen and this loading
// screen will be unmounted and thrown away.
dispatch({ type: 'RESTORE_TOKEN', token: userToken });
};
bootstrapAsync();
}, []);
const authContext = React.useMemo(
() => ({
signIn: async (data) => {
// In a production app, we need to send some data (usually username, password) to server and get a token
// We will also need to handle errors if sign in failed
// After getting token, we need to persist the token using `SecureStore` or any other encrypted storage
// In the example, we'll use a dummy token
dispatch({ type: 'SIGN_IN', token: data.signIn });
},
signOut: () => dispatch({ type: 'SIGN_OUT' }),
signUp: async (data) => {
// In a production app, we need to send user data to server and get a token
// We will also need to handle errors if sign up failed
// After getting token, we need to persist the token using `SecureStore` or any other encrypted storage
// In the example, we'll use a dummy token
dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
},
}),
[]
);
return (
<AuthContext.Provider value={authContext}>
<ApolloProvider client={client}>
<NavigationContainer>
<Stack.Navigator>
{state.isLoading ? (
// We haven't finished checking for the token yet
<Stack.Screen name="Splash" component={SplashScreen} />
) : state.userToken == null ? (
// No token found, user isn't signed in
<Stack.Screen
name="SignIn"
component={SignInScreen}
options={{
title: 'Sign in',
// When logging out, a pop animation feels intuitive
animationTypeForReplace: state.isSignout ? 'pop' : 'push',
}}
/>
) : (
// User is signed in
<Stack.Screen name="Home" component={HomeScreen} />
)}
</Stack.Navigator>
</NavigationContainer>
</ApolloProvider>
</AuthContext.Provider>
);
}
I have tried looking at other posts with similar errors but I can't manage to find one that makes it work as expected.
AuthContext.js
import React from "react";
const AuthContext = React.createContext();
export default AuthContext;
const AuthContextProvider = ({ children }) => {
const authContext = React.useMemo(
() => ({
signIn: async (data) => {
await AsyncStorage.setItem('userToken', data.token);
await AsyncStorage.setItem('user', JSON.stringify(data));
dispatch({type: 'SIGN_IN', token: data.token, user: data});
},
signOut: async () => {
await AsyncStorage.removeItem('userToken');
await AsyncStorage.removeItem('user');
dispatch({type: 'SIGN_OUT'});
}
}),
[]
);
return (
<AuthContext.Provider
value={{
authContext
}}
>
{children}
</AuthContext.Provider>
);
};
Then in App.js
import { AuthContextProvider } from './AuthContext';
....
return (
<PaperProvider theme={theme}>
<AuthContextProvider>
<SafeAreaProvider>
<NavigationContainer>
<DetailsScreen />
</NavigationContainer>
</SafeAreaProvider>
</AuthContextProvider>
</PaperProvider>
);
Then in DetailsScreen.js
import { AuthContext } from "../AuthContext";
constructor(props) {
const {context} = useContext(AuthContext);
console.log("-----------------------------", context); // returns undefined
super(props, context);
}
The error this block of code is causing is:
Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined.
I am out of ideas as to what could be wrong.
The context AuthContext that you created in AuthContext.js exports the context as default, but you import it as named-import, ie using import {} from. So the useContext hook takes as an argument a null instead of the actual context created by React.createContext()
Then be careful that const {context} = useContext(AuthContext); is also wrong as the hook will return the object {authContext: {...}} which means that you have to do const {authContext} = useContext(AuthContext);
In the Provider you can avoid passing value={{authContext}} and instead pass value={authContext} then you can just const authContext = useContext(AuthContext);
I have been learning React Native through a course that is not using version 6, and has deprecated usage of Context / Provider mechanisms.
As a result, I am unable to upgrade my code to work with the latest good practices.
Here is the overview of what I am trying to do.
const App = (props) => {
const isAuth = true;
return (
<AuthProvider>
<NavigationContainer>
{isAuth && <MainStackScreens />}
{!isAuth && <LoginStackScreens />}
</NavigationContainer>
</AuthProvider>
);
};
Basically I want to replace my isAuth variable with an access to the state communicated by AuthProvider (and use the token!), which is what it is intended for I believe.
Reading further documentation, I realized that thanks to the Provider it is indeed possible to have access to the state throughout the application with an import of the Context provided to any child. However, I don't understand how to have access to it right there in the App.js on my NavigationContainer.
Could you help?
Some additional resources...
My Provider is imported in the App.js like this :
import { Provider as AuthProvider } from './context/AuthContext';
and defined like this :
import createDataContext from "./createDataContext";
import AsyncStorage from '#react-native-async-storage/async-storage';
const authReducer = (state, action) => {
switch(action.type) {
case 'add_error':
return {...state, errorMessage: action.payload}
case 'token_stored':
return { errorMessage:'', token: action.payload}
case 'logout':
return { token:null, errorMessage:''}
default:
return state;
}
} ;
const signup = (dispatch) => async ( {username, email, password}) => { // some code } ;
const signin = (dispatch) => async ( {username, password}) => { // some code } ;
export const { Provider, Context} = createDataContext(
authReducer,
{signin, signup},
{ token: null, errorMessage: '' }
);
The createDataContext is defined like this :
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 key in actions) {
boundActions[key] = actions[key](dispatch);
}
return (
<Context.Provider value={{ state, ...boundActions }}>
{children}
</Context.Provider>
);
};
return { Context, Provider };
};
Create another screen
return (
<AuthProvider>
<NavigationContainer>
<Screens />
</NavigationContainer>
</AuthProvider>
);
Screens = () = >
// get auth from context
return (
<>
{isAuth && <MainStackScreens />}
{!isAuth && <LoginStackScreens />}
</>
);
I'm trying to test my LoginScreen with Jest and Typescript. I use redux and redux-persist for storage and have set the storage up to use AsyncStorage as part of the config. I suspect that redux-persist is attempting to rehydrate after the built-in time-out function it uses runs out and tries to set storage to default storage? I'm getting the following error:
console.error
redux-persist: rehydrate for "root" called after timeout. undefined
undefined
at _rehydrate (node_modules/redux-persist/lib/persistReducer.js:70:71)
at node_modules/redux-persist/lib/persistReducer.js:102:11
at tryCallOne (node_modules/promise/setimmediate/core.js:37:12)
at Immediate._onImmediate (node_modules/promise/setimmediate/core.js:123:15)
Currently my test looks like this:
describe('Testing LoginScreen', () => {
it('should render correctly', async () => {
const { toJSON } = render(<MockedNavigator component={LoginScreen} />);
await act(async () => await flushMicrotasksQueue());
expect(toJSON()).toMatchSnapshot();
});
});
and my MockNavigator looks like this:
type MockedNavigatorProps = {
component: React.ComponentType<any>;
params?: {};
};
const Stack = createStackNavigator();
const MockedNavigator = (props: MockedNavigatorProps) => {
return (
<MockedStorage>
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name='MockedScreen'
component={props.component}
initialParams={props.params}
/>
</Stack.Navigator>
</NavigationContainer>
</MockedStorage>
);
};
export default MockedNavigator;
Here is the way I'm creating my storage:
import 'react-native-gesture-handler';
import * as React from 'react';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import { store, persistor } from '../src/AppState/store';
type MockedStorageProps = {
children: any;
};
const MockedStorage = (props: MockedStorageProps) => {
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
{props.children}
</PersistGate>
</Provider>
);
};
export default MockedStorage;
I resolved this same error using this advice from an issue on the redux-persist repo: https://github.com/rt2zz/redux-persist/issues/1243#issuecomment-692609748.
(It also had the side-effect of avoiding logging errors in test from redux-logger.)
jest.mock('redux-persist', () => {
const real = jest.requireActual('redux-persist');
return {
...real,
persistReducer: jest
.fn()
.mockImplementation((config, reducers) => reducers),
};
});
#alexbrazier:
It basically just bypasses redux-persist by returning the reducers
directly without wrapping them in redux-persist.
I am trying to create a LoginForm in which I am placing the the UI using react-native, but the backend logic is through redux framework. I have integrated with the firebase libraries and am trying to make an async call to the firebase using the action creators and reducers through redux-thunk.
App.js
.........
.........
render()
{
const store = createStore(reducers, {}, applyMiddleware(ReduxThunk));
return(
<Provider store={ store } >
<LoginForm />
</Provider>
);
}
LoginForm.js
class LoginForm extends Component
{
.........
.........
onButtonPress () {
const { email, password } = this.props;
this.props.loginUser({ email, password });
}
render()
{
<CardSection>
<Button onPress={this.onButtonPress.bind(this)} >
Login
</Button>
</CardSection>
}
const mapStateToProps = ( state ) => {
return {
email: state.auth.email,
password: state.auth.password
};
};
export default connect (mapStateToProps, { emailChanged, passwordChanged, loginUser })(LoginForm);
Actions
index.js
export const loginUser = ({ email, password }) => {
console.log("Shikher1");
return (dispatch) => {
firebase.auth().signInWithEmailAndPassword( email, password ).then( user => {
dispatch ({ type: 'LOGIN_USER_SUCCESS' , payload: user });
});
};
};
Nothing is mentioned in the Reducer as such, I just wanted to make sure that the action gets triggered and the Async call is made. But nothing gets happened here. As I printed from the console.logs I can see that the callback function is getting executed and it calls the action creator too, but the firebase statement is not getting executed, as after its execution, it will return an object. Why is the firebase statement is not getting executed?
Where am I going wrong here?
in in your LoginForm.js, try to add these lines
const mapStateToProps = ( state ) => {
return {
email: state.auth.email,
password: state.auth.password
};
};
const mapDispatchToProps = dispatch => ({
emailChanged: payload => dispatch(emailChanged(payload)),
passwordChanged: payload => dispatch(passwordChanged(payload)),
loginUser : payload => dispatch(loginUser (payload))
})
export default connect (mapStateToProps ,mapDispatchToProps )(LoginForm);