Rerender AppNavigator on state change - reactjs

I am trying to render certain nav stacks depending on a isAuthenticated state. The problem that I am having is that AppNavigator is only rendered on the first render and not with any other changes and I am not sure why. I have tried a useEffect in the AppNavigator component to set a secondary local state with the callback being isAuthenticated but no go. I put everything pertinent below. I appreciate any advice.
I have an AppNavigator that is being rendered in my app.tsx file.
return (
<ToggleStorybook>
<ApolloProvider client={client}>
<RootStoreProvider value={rootStore}>
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
<ErrorBoundary catchErrors={"always"}>
<AppNavigator
initialState={initialNavigationState}
onStateChange={onNavigationStateChange}
/>
</ErrorBoundary>
</SafeAreaProvider>
</RootStoreProvider>
</ApolloProvider>
</ToggleStorybook>
)
The AppNavigator is returning
export const AppNavigator = (props: NavigationProps) => {
const { isAuthenticated } = useStores()
const colorScheme = useColorScheme()
useBackButtonHandler(canExit)
return (
<NavigationContainer
ref={navigationRef}
theme={colorScheme === "dark" ? DarkTheme : DefaultTheme}
{...props}
>
<Stack.Navigator
screenOptions={{
headerShown: false,
}}
>
{isAuthenticated ? (
<Stack.Screen name="main" component={MainTabs} />
) : (
<Stack.Screen name="signup" component={SignUpStack} />
)}
</Stack.Navigator>
</NavigationContainer>
)
}
I am using mob-state-x-tree for state management and have a setUser action that is called onAuthStateChanged per the firebase Auth docs. I'm using email and password login and sign up. I've logged the auth state changes they are working as expected.
function onAuthStateChanged(user: any) {
if (user) {
if (rootStore) {
rootStore.setUser(user)
console.log("we have passed user to root store")
}
}
The setUser action sets a state isAuthenticated in a try catch
setUser: flow(function* (firebaseUser) {
try {
const idToken = yield firebaseUser.getIdToken()
yield AsyncStorage.setItem(
'#lessns:token',
idToken
);
self.isAuthenticated = true
self.user = {id: firebaseUser.uid}
} catch(err) {
console.log(err, 'this is the first time ')
self.isAuthenticated = false
}
}),

You need to make your AppNavigator component into an observer so that it will re-render when observable data it depends on changes.
export const AppNavigator = observer((props: NavigationProps) => {
// ...
})

try to put AppNavigator component into an observer?

Related

Firebase.auth().onstateChanged() not working after clearing browser cache

I cleared my browser cache and now my app cant login
export function IsUserRedirect({ user, loggedInPath, children, ...rest}){
return (
<Route
{...rest}
render={() => {
if(!user){
return children;
}
if(user){
return (
<Redirect
to={{
pathname: loggedInPath
}}
/>
)
}
return null;
}}
/>
)
}
export function ProtectedRoute({ user, children, ...rest}){
return(
<Route
{...rest}
render={({location}) => {
if(user){
return children;
}
if(!user){
return (
<Redirect
to={{
pathname: 'signin',
state: { from : location}
}}
/>
)
}
return null;
}}
/>
)
}
I think it stored my login info on the browser as a localstorage but after clearing it still recognizes it as the user is logged in and takes me to the next page.
But on the next page i have kept a loading state for getting user data, as it doesnt has any user it just keeps loading and goes nowhere. can someone help
export default function useAuthListener(){
const [user, setUser] = useState(JSON.parse(localStorage.getItem('authUser')));
const {firebase} = useContext(FirebaseContext);
useEffect(() => {
const listener = firebase.auth().onAuthStateChanged((authUser) => {
if(authUser){
localStorage.setItem('authUser', JSON.stringify(authUser));
setUser(authUser);
}else {
localStorage.removeItem('authUser');
setUser(null);
}
});
return ()=> listener();
}, []);
return { user};
}
just a quick suggestion:
localStorage.clear();
Source:
(Another aproach might be a reboot, to see if it acts differently...)
Greetings,
Alexander

React Hook triggers re-render but React Navigation doesn't update screens

I wrote a useAuth hook. From this hook I use the var partnerId in my App.js to build the StackNavigator Screens. I expect, when I set/unset partnerId that the user gets automatically navigated to the Pairing or Dashboard component, because React Navigation will remove one of it. (As described here after the first code snippet)
So when I run the App, I see the dashboard, because I used addPartner('test');.
When I click the button on the dashboard, I see that the component re-rendered because the <Text> node changed from Partner to No Partner.
But I expect to get redirected to the Pairing component!
App.js
const AppStack = createStackNavigator();
const App = () => {
const [myId, partnerId, addPartner, removePartner] = useAuth();
addPartner('test'); // for testing right now
return (
<NavigationContainer>
<AppStack.Navigator>
{!partnerId ? (
<AppStack.Screen
name="Pairing"
component={Pairing}
options={{headerShown: false}}
/>
) : (
<AppStack.Screen
name="Dashboard"
component={Dashboard}
options={{title: 'Dashboard'}}
/>
)}
</AppStack.Navigator>
</NavigationContainer>
);
};
export default App;
dashboard.js
const Dashboard = () => {
const [myId, partnerId, addPartner, removePartner] = useAuth();
return (
<SafeAreaView>
<ScrollView>
<Text>Dashboard</Text>
{partnerId ? <Text>Partner</Text> : <Text>No Partner</Text>}
<Button
title="Remove Partner Id"
onPress={() => {
removePartner();
}}
/>
</ScrollView>
</SafeAreaView>
);
};
export default Dashboard;
useAuth.js
const PARTNERID_KEY = 'partnerId';
const MYID_KEY = 'myId';
const useAuth = () => {
const [myId, setMyId] = useState(null);
const [partnerId, setPartnerId] = useState(null);
const removePartner = async () => {
await AsyncStorage.removeItem(PARTNERID_KEY);
setPartnerId(null);
console.log('Removed Partner', partnerId);
};
const addPartner = async (newPartnerId) => {
if (!newPartnerId) {
console.error('newPartnerId is null/undefined');
return false;
}
if (newPartnerId.toUpperCase() === myId.toUpperCase()) {
console.error('newPartnerId and myId are equal');
return false;
}
await AsyncStorage.setItem(PARTNERID_KEY, newPartnerId);
setPartnerId(newPartnerId);
console.log('Added Partner', partnerId);
return true;
};
useEffect(() => {
const init = async () => {
// partner
const storagePartnerId = await AsyncStorage.getItem(PARTNERID_KEY);
if (storagePartnerId) {
setPartnerId(storagePartnerId);
}
// self
let storageMyId = await AsyncStorage.getItem(MYID_KEY);
if (!storageMyId) {
storageMyId = await UUIDGenerator.getRandomUUID();
await AsyncStorage.setItem(MYID_KEY, storageMyId);
}
setMyId(storageMyId);
console.log('Auth init', myId, partnerId);
};
init();
}, [myId, partnerId]);
return [myId, partnerId, addPartner, removePartner];
};
export default useAuth;
I found out that it's not possible to use a Hook like a global singleton (e.g. like a Angular Service), because it is not a singleton!
The answer is to call useAuth() once in App.js and use React.createContext() to pass the values to child components.
App.js
const AppStack = createStackNavigator();
export const AuthContext = React.createContext();
const App = () => {
const [myId, partnerId, addPartner, removePartner] = useAuth();
const authContext = {myId, partnerId, addPartner, removePartner};
addPartner('test'); // testing
return (
<AuthContext.Provider value={authContext}>
<NavigationContainer>
<AppStack.Navigator>
{!partnerId ? (
<AppStack.Screen
name="Pairing"
component={Pairing}
options={{headerShown: false}}
/>
) : (
<AppStack.Screen
name="Dashboard"
component={Dashboard}
options={{title: 'Dashboard'}}
/>
)}
</AppStack.Navigator>
</NavigationContainer>
</AuthContext.Provider>
);
};
export default App;
dashboard.js
const Dashboard = () => {
const foo = React.useContext(AuthContext);
return (
<SafeAreaView>
<ScrollView>
<Text>Dashboard</Text>
{foo.partnerId ? <Text>Partner</Text> : <Text>No Partner</Text>}
<Button
title="Remove Partner Id"
onPress={() => {
foo.removePartner();
}}
/>
</ScrollView>
</SafeAreaView>
);
};
export default Dashboard;

Expo/React Native - Can't redirect to homescreen after login

I am working on a mobile app with Expo/React Native and trying to have a login screen for sign in. Once signed in,the user will be redirected to the homescreen.
However I have not been able to configure it with setting the state upon successful gaining the credentials for "isLoggedIn:true." On the stack navigator, I tried to set it up where it will show different screens (Home and Third Screen) if logged in while the LoginScreen will be if logged in is false. How do I configure it to properly redirect to the homescreen upon successful login.
export default function App() {
const [state, setstate] = useState({
isSignedIn:false,
})
return (
<NavigationContainer>
<Stack.Navigator>
{state.isSignedIn == false ? (
<>
<Stack.Screen name='Login' component={LoginScreen} />
</>
) : (
<>
<Stack.Screen name='Home' component={HomeStack} options={({ route }) => ({
headerTitle: getHeaderTitle(route) })} />
<Stack.Screen name='ThirdScreen' component={ThirdScreen}/>
</>
)}
</Stack.Navigator>
</NavigationContainer>
);
}
Below is my code for the login on the LoginScreen.js:
render() {
function handleSubmit({ props }) {
let formData = new FormData();
formData.append('username',this.state.username);
formData.append('password',this.state.password);
fetch('<My login API from the server side>', {
method: 'POST',
body:formData
}).then(response => {
if(response.status == 200) {
response.body.getReader().read().then(({value, done}) => {
const decoder = new TextDecoder('utf-8')
const keyitems = decoder.decode(value);
//convert to JSON
const obj = JSON.parse(keyitems);
//get individual values from string
this.setState({
userToken:obj.key.toString(),
username:obj.user.username,
user_id:obj.user.id,
user_image:obj.user.userprofile.user_image,
isSignedIn:true
})
//refresh or redirect to hopefully get to home
//assuming the state has been switched to logged
this.props.navigation.navigate('Home')
})
} else {
//issue alert to say try again
console.log("bad")
}
});
}
you don't need to navigate to the Home screen manually by calling navigation.navigate('Home'). React Navigation will automatically navigate to the Home screen when isSignedIn becomes true.
https://reactnavigation.org/docs/auth-flow/
It seems like you have written a hook call 'setstate' but you have not used that in the child component. To make it work, you would need to pass the function argument of the hook in the parent component to the child component and call that to set the signed in state.
Try modifying your code like this
export default function App() {
const [state, setState] = useState({
isSignedIn:false,
})
return (
<NavigationContainer>
<Stack.Navigator>
{state.isSignedIn == false ? (
<>
<Stack.Screen name='Login' component={LoginScreen} setState={setState} />
</>
) : (
<>
<Stack.Screen name='Home' component={HomeStack} options={({ route }) => ({
headerTitle: getHeaderTitle(route) })} />
<Stack.Screen name='ThirdScreen' component={ThirdScreen}/>
</>
)}
</Stack.Navigator>
</NavigationContainer>
);
}
and
render() {
function handleSubmit({ props }) {
let formData = new FormData();
formData.append('username',this.state.username);
formData.append('password',this.state.password);
fetch('<My login API from the server side>', {
method: 'POST',
body:formData
}).then(response => {
if(response.status == 200) {
response.body.getReader().read().then(({value, done}) => {
const decoder = new TextDecoder('utf-8')
const keyitems = decoder.decode(value);
//convert to JSON
const obj = JSON.parse(keyitems);
//get individual values from string
props.setState({
userToken:obj.key.toString(),
username:obj.user.username,
user_id:obj.user.id,
user_image:obj.user.userprofile.user_image,
isSignedIn:true
})
})
} else {
//issue alert to say try again
console.log("bad")
}
});
}

Getting react passed props & redux state at same time in component

Im trying to get access to redux state but im also need props that passed from routing.
The example is: i need that props
const DefaultLayout = props => {
return (
<div>
</div>
)
}
because
<Route
path="/"
name="Home"
render={props => <DefaultLayout {...props} />}
/>
when i add redux state like: {auth: {user}} to access user data, its not working.
const DefaultLayout = (props , {auth: {user}}) => {
return (
<div>
</div>
)
}
...
DefaultLayout.propTypes = {
auth: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
auth: state.auth
});
export default connect(mapStateToProps)(DefaultLayout);
if i delete that props i will getting pathname error, any explanation and help? new to react.
I think you need to access the props this way :
const DefaultLayout = ({auth, location, history, ...otherProps}) => {
//example : console.log(otherProps.match);
return (
<div>
{auth.user}
</div>
)
}
Props passed by the Route component are merged with the props added by redux.
const DefaultLayout = (props , {auth: {user}}) => {
Components just get passed a single variable: props. If you want to access auth.user, it's found at props.auth.user. It's put there by mapStateToProps in cooperation with connect
const DefaultLayout = (props) => {
const { location, history, match, auth } = props;
return (
<div>
// something with the variables
</div>
)
}
const mapStateToProps = state => ({
auth: state.auth
})
export default connect(mapStateToProps)(DefaultLayout);

Simulate localStorage data in test

I am using Create React App.
I am trying to simulate isLoggedIn behaviour in my component to get all lines code coverage.
To do that localStorage key: user must exist with data.accessToken
I tried set localStorage data in the test but it is not working. the same method actually working in isLoggedIn function and generate 100% line coverage.
isLoggedIn function
export const isLoggedIn = () => {
const userFromLocalStorage = store('user');
return _get(userFromLocalStorage, 'data.accessToken', false);
};
PrivateRoute.js:
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route
{...rest}
render={props =>
isLoggedIn() ? (
<Component {...props} />
) : (
<Redirect to={{ pathname: 'login' }} />
)
}
/>
);
PrivateRoute.spec.js
import store from 'store2';
describe('PrivateRoute Logged In', () => {
store('user', {
data: {
accessToken: 'dfg',
},
});
const ShallowPrivateRoute = shallow(
<PrivateRoute path="/" name="Home" component={TestComponent} />
);
it('should cover logged in case', () => {
expect(ShallowPrivateRoute).toBeDefined();
});
});
Is there the way I can mock isLoggedIn function to return true just for one test??
What is the best way to test that kind of behaviour?
You could mock the entire file like this:
jest.mock("you-module", () =>({...methodsMock}));
or you could recieve isLoggedIn in props, that way you only need to pass a mock function when you render your component in test.
<Component isLoggedIn={jest.fn().mockReturnValue(true)} />

Resources