When to navigate from Authentication SwitchNavigator? - react-native

I'm setting up authentication flow in my React Native app, and I can pretty much get everything to work, except I can't figure out the actual proper place to navigate to the logged in stack with this.props.navigation.navigate("Main");.
I have a Switch Navigator at the top of my app rendering the auth stack (its own switch nav) and the main stack:
// App.js
const AppContainer = createAppContainer(
createSwitchNavigator({
Auth: AuthNavigator,
Main: MainTabNavigator,
})
);
export default class App extends Component {
render() {
return (
<Provider store={store}>
<View style={styles.container}>
<AppContainer />
</View>
</Provider>
);
}
}
The login screen is a complete dummy setup for now (it calls a local api and succeeds no matter what). Note the conditional at the start of the render method:
// LoginView.js
class LoginView extends Component {
state = { username: "", password: "" };
handleLogin() {
this.props.login.call(this);
}
render() {
if (this.props.user) {
this.props.navigation.navigate("Main");
return null;
}
return (
<View style={styles.container}>
<Overlay>
<View style={styles.modalContainer}>
<DotIndicator color={"darkgrey"} />
<Text>Logging in...</Text>
</View>
</Overlay>
// ... input components ...
<Button
title="Login"
onPress={this.handleLogin.bind(this)}
/>
</View>
);
}
}
export default connect(
state => ({
isLoading: state.auth.isLoading,
user: state.auth.user,
error: state.auth.error
}),
{ login }
)(LoginView);
Now, this technically works but I get the error Warning: Cannot update during an existing state transition (such as withinrender). Render methods should be a pure function of props and state..
I understand this to be thrown because I'm calling the navigate inside of the render. Makes sense.
I've tried navigating from my login action:
export function login() {
return dispatch => {
dispatch({ type: "LOGIN_START" });
fetch(url) // this will need to be a POST session (not a GET user)
.then(res => res.json())
.then(users => {
dispatch({ type: "LOGIN_SUCCESS", payload: users[0] });
console.log("Login succeeded");
this.props.navigation.navigate("Main");
})
.catch(error => {
console.warn("login failed");
dispatch({ type: "LOGIN_FAILURE", payload: error });
});
};
}
And it works but of course is very wrong in terms of separation of concerns, and this kind of solution also doesn't really stretch as far when I try use it for handling login failure.
I've also thought about doing it in my handleLogin() function (called by my login button), with and without async/await:
async handleLogin() {
await this.props.login.call(this);
console.log("Navigating to main screen");
this.props.navigation.navigate("Main");
}
But the navigation happens before the action finishes and the user is logged in, which is of course unacceptable once the auth flow is real. (Plus, when I do it with async/await, "Login Succeeded" gets logged twice! Beats me). It does feel like I'm on the right track doing it in my handleLogin() function.
Where do I put my navigate call?

I appear to have found something that works, but it takes steps away from Redux, and that makes me slightly uncomfortable.
I've taken all the fetching out of the Redux action and put it right in my LoginView. I'm considering this okay in Redux terms, because the only place the program needs to know about the authorization process and state is in the authorization flow itself (in the LoginView itself, for now). The only thing the store and the rest of the app need to know is the info for the user.
So I've replaced my Redux login action with a simple assignUser action. I've refactored the fetching into performLogin, which I call from the old handleLogin, after posting a state change to activate my "Logging in..." overlay.
// authActions.js
export function assignUser(user) {
return dispatch => {
console.log("user", user);
dispatch({ type: "LOGIN_SUCCESS", payload: user });
};
}
// from LoginView.js
performLogin() {
fetch("http://127.0.0.1:3000/users") // this will need to be a POST session (not a GET user)
.then(res => res.json())
.then(users => {
console.log("Login succeeded");
this.props.assignUser(users[0]);
console.log("Navigating to main screen");
this.props.navigation.navigate("Main");
})
.catch(error => {
console.warn("login failed", error);
this.setState({isLoading: false})
});
}
handleLogin() {
this.setState({ isLoading: true }, this.performLogin);
}
The rest of the LoginView component is the same as in my original post.
I'd still love to know if anyone has any methods for accomplishing this that keep the actions fully within Redux.

Related

How to update state on App start in react native

I am trying to read from the AsyncStorage and update my store ( using easy-peasy ) on app start.
My thought process was to call fetch the data from AsyncStorage using useEffect with the second argument of an empty array to only fetch once on app start and update the store with that value using an action.
But that doesn't work i get the error invalid hook call. Any insights on how to solve this or what the correct approach would be are appreciated !
App.ts
export default function App() {
useEffect(() => {
readData();
}, []);
return (
<StoreProvider store={store}>
<SafeAreaProvider>
<Navigation />
<StatusBar />
</SafeAreaProvider>
</StoreProvider>
);
}
// This needs to be called when app is started
const readData = async () => {
try {
const secret = await storage.getItem('secret');
const initializeState = useStoreActions(
(actions) => actions.initializeState
);
initializeState({
payload: {
secret,
},
});
console.log("executed action")
} catch (e) {
console.log('Failed to fetch the input from storage', e);
}
};
STORE
initializeState: action((state, payload) => {
state.secret = payload.secret
}),
ERROR
Failed to fetch the input from storage [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
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.]
export default function App() {
useEffect(() => {
setTimeout(()=>{ // add setTimeout() may be this work for you
readData();
},500)
}, []);
return (
<StoreProvider store={store}>
<SafeAreaProvider>
<Navigation />
<StatusBar />
</SafeAreaProvider>
</StoreProvider>
);
}
// This needs to be called when app is started
const readData = async () => {
try {
const secret = await storage.getItem('secret');
const initializeState = useStoreActions(
(actions) => actions.initializeState
);
initializeState({
payload: {
secret,
},
});
console.log("executed action")
} catch (e) {
console.log('Failed to fetch the input from storage', e);
}
};
You need to move you readData function into the App component since you're using a hook (useStorageActions) inside that function and you can only call hooks at the top level. You should take a look at the rules of react hooks.

Unable to evaluate item.id in React Native FlatList

I am trying to render a list of exercises using React Native's FlatList component, and I'm currently unable to successfully edit the name of the exercise. Everything runs smoothly upon the initial render. However, when I try to edit the exercise, the subsequent re-render returns the error undefined is not an object (evaluating 'item.id'). If I check my local database, the data is successfully edited, but rendering it to the screen is another issue. If I were to dismiss the error display and reload the app, however, the expected changes have been made to the exercise.
I've seen previous posts/responses surrounding this issue, but from what I can tell, I've tried the suggested solutions to no avail.
Here is the component responsible for rendering the FlatList:
function Exercises({ fetchExercises }) {
const dispatch = useDispatch()
const exercises = useSelector(state => state.exercises) // **passed to data prop**
useEffect(() => {
dispatch(fetchExercises())
}, [dispatch])
<FlatList
data={exercises}
keyExtractor={item => item.id.toString()}
renderItem={({ item }) => {
return <ExerciseListItem item={item} />
}}
/>
Below is the action I'm dispatching in the useEffect:
export const editExercise = (id, changes) => dispatch => {
dispatch({ type: EXERCISE_START })
axios.put(<endpoint>, { name: changes })
.then(res => {
dispatch({ type: EDIT_EXERCISE_SUCCESS, payload: res.data })
.catch(err => {
dispatch({ type: EDIT_EXERCISE_FAIL, payload: err })
}
}
...and here is my reducer:
const initialState = {
exercises: [],
}
const exercisesReducer = (state = initialState, action) => {
switch (action.type) {
case EDIT_EXERCISE:
return {
...state,
isLoading: false,
exercises: state.exercises.map(exercise=> {
exercise.id === action.payload.id ? action.payload : exercise
}
}
}
}
For what it's worth, I'm able to successfully add a new exercise to the list. Not sure if that would make a difference, but it's worth mentioning. Also, here is a similar question that someone asked about a year ago with what would appear to be the solution to my issue, but I'm still not getting the expected results.
Any help would be greatly appreciated!
I think the issue is that you're not returning anything in your map which is leading to this error.
exercises: state.exercises.map(exercise=> {
return exercise.id === action.payload.id ? action.payload : exercise
}

Defer notification deep linking until after react-native expo app is fully loaded

I would like to defer the deep-link navigation event tied to a notification until the app is fully loaded after my react-native app is opened on notification click.
Currently, my notification listener is in my App.tsx file. The deep linking works as expected when the app is backgrounded, but when the notification triggers the app to open, the navigation event is kicked off before the App has a chance to fully load. This means that although I do get deep linked to the correct location, some of my assets aren't yet loaded and my auth logic is all bypassed.
Is there a way to have a notification open the app, but wait until everything is loaded (specifically the AppLoading component has finished running its functions) to navigate to the deep linking location? I can think of some hack-y seeming ways to do this but is there an established pattern that is commonly used?
Ok so I'm not sure if this is the best way to handle this but I found a way to get my notifications working.
(1) I created an action called registerAppLoaded and a state variable appLoaded that I dispatch once my AppLoading component finishes its startAsync functions.
(2) When a notification comes in, I first check to see if appLoaded is true. If so, I navigate to the destination sent along with the notification. If not, I put the notification in the store and carry on with firing up the app.
(3) in the AppLoading onFinish function, I check to see if there is a notification. If so, and it is marked new, I grab it and use the params to navigate. Then I dispatch an action that sets navigation.new = false
Seems to work exactly the way i need it to!
Here is my code if anyone else is dealing with this issue:
App.tsx:
export default class App extends React.Component {
componentDidMount() {
Notifications.addListener((notification) => {
if (notification.origin === "received") {
return;
} else {
const appLoaded = store.getState().general.appLoaded
if (appLoaded) {
NavigationService.navigate(notification.data.navigate, {id:notification.data.id , shouldRefresh: true})
} else {
// save notification as new
store.dispatch(addNotification(notification, true));
}
};
})
}
render() {
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<View style={styles.container}>
{Platform.OS === "ios" && <StatusBar barStyle="default" />}
<ErrorBoundary backup={<AppError />}>
<AppNavigator />
</ErrorBoundary>
</View>
</PersistGate>
</Provider>
);
}
}
AppLoading.tsx:
class AppLoadingScreen extends React.Component {
_loadResourcesAsync = async() => {
await this.props.fetchUserOnboarding(this.props.userId);
if (this.props.userDevice && this.props.userDevice.id) {
this.props.registerUserDevice(this.props.userId, this.props.userDevice.id)
}
// register that the app has finished loading in the store; this will be used to determine if a notification's deep
// link should be immediatedly navigated to or if the navigation even shuold be deferred until after the app
// finishes loading
await this.props.registerAppLoaded()
};
_handleFinishLoading = async () => {
// if a notification triggers the event listener but the app is not yet fully loaded, the deep link will be
// navigated to here instead of directly from the listener
const notification = this.props.userNotifications && this.props.userNotifications[0]
if (notification && notification.new && notification.origin != 'recieved') {
this.props.addNotification(notification, false) // set notification.new = false
this.props.navigation.navigate(notification.data.navigate, {id: notification.data.id, shouldRefresh: true})
} else if (this.props.showGoalsPrompt) {
this.props.navigation.navigate("Goal");
} else {
this.props.navigation.navigate("HomeFeed");
}
};
render() {
return (
<AppLoading
startAsync={this._loadResourcesAsync}
onFinish={this._handleFinishLoading}
/>
);
}
}
actions.tsx:
export const registerAppLoaded = () => {
return dispatch => {
dispatch({
type: types.REGISTER_APP_LOADED,
payload: true
});
};
}
export const addNotification = (notification, isNew=false) => {
return dispatch => {
notification.new = isNew // indicate whether this is a new notification or if it has been seen
dispatch({
type:types.ADD_USER_NOTIFICATION,
payload: notification,
})
};
}

React Native: TypeError: this.state.schedule.map is not an object

Hey I am new to React Native and currently I'm trying to put data in a picker using data from API. I'm confused that it got error say TypeError: null is not an object (evaluating this.state.schedules.map). Is there something wrong with the state or is there any concept that I misunderstood
Here is fetch API
export function getSchedule (token, resultCB) {
var endpoint = "/api/getList"
let header = {
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": "Bearer " + token
};
return dispatch => {
return fetchAPI(endpoint, 'GET', header)
.then((json) => {
dispatch({ type: t.SCHEDULE, schedules: json.datas.description });
resultCB(json.schedules)
})
.catch((error) => {
dispatch({ type: types.EMPTY_SCHEDULE });
resultCB(error)
})
}
}
this is where i put my picker
export const mapStateToProps = state => ({
token: state.authReducer.token,
message: state.authReducer.message,
schedules: state.authReducer.schedules
});
export const mapDispatchToProps = (dispatch) => ({
actionsAuth: bindActionCreators(authAction, dispatch)
});
class Change extends Component {
constructor(){
super();
this.state={
staffId: "",
schedule: '',
type_absen: 1,
schedules: null
}
}
componentDidMount(){
this.props.actionsAuth.getSchedule(this.props.token);
}
render() {
return (
<View style={styles.picker}>
<Picker
selectedValue={this.state.schedule}
style={{backgroundColor:'white'}}
onValueChange={(sch) => this.setState({schedule: sch})}>
{this.state.schedules.map((l, i) => {
return <Picker.Item value={l} label={i} key={i} /> })}
</Picker>
</View>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Change);
This isn’t a React Native specific error. You initialized schedules to null so on first render, you try to call .map on null. That’s what is causing your error.
You fetch your data correctly in componentDidMount but that lifecycle method will fire after the initial render.
One common way to fix this is to initialize schedules to an empty array.
First initialise schedules: [] in the state with empty array, not with the null.
Fetching data in componentDidMount() is correct. ComponentDidMount() will be called after the first render of component so you have to update the state in the component from the updated store.
you can check whether props is changing or not in componentWillReceiveProps (depreciated) or in the latest alternative of componentWillReceiveProps method that is getDerivedStateFromProps().
Below is the syntax for both
componentWillReceiveProps(nextProps) {
if (this.props.schedules !== nextProps.schedules) {
this.setState({ schedules: nextProps.schedules });
}
}
static getDerivedStateFromProps(nextProps, prevState){
if (nextProps.schedules !== prevState.schedules) {
return { schedules: nextProps.schedules };
}
else return null; // Triggers no change in the state
}
Make sure your component should connected to store using connect

How can I change a parent state through a react navigation component?

I'm new to React Native and having trouble figuring out how to accomplish this. Currently I have an app structure something like this:
App.js -> Authentication.js -> if(state.isAuthenticated) Homepage.js, else Login.js
I'm currently changing the isAuthenticated state on a logout button on the homepage. I'm now trying to add in a drawer navigator to the app, which would get returned to the authentication page in place of the homepage. So I'm not sure how to pass the state change through the drawernavigator component to the Authentication page.
Currently my Homepage has a button that has:
onPress={() => this.props.logout()}
And the authentication page has:
export default class Authentication extends Component {
constructor(props){
super(props);
this.state = {
isAuthenticated: false,
isLoading: false
}
this.login = this.login.bind(this);
this.logout = this.logout.bind(this);
}
login() {
AsyncStorage.setItem("user", JSON.stringify({email: this.state.email, password: this.state.password}))
.then(results => {
this.setState({isAuthenticated: true});
});
}
logout() {
AsyncStorage.clear()
.then(result => {
this.setState({isAuthenticated: false});
});
}
componentDidMount(){
this.setState({isLoading: true});
AsyncStorage.getItem("user")
.then(results => {
const data = JSON.parse(results);
if (data) {
this.setState({isAuthenticated: true});
}
this.setState({isLoading: false});
});
}
render() {
if (this.state.isLoading){
return(
<Splashpage />
);
}
if (!this.state.isAuthenticated){
return (
<Login login={this.login}/>
);
}
return (
<Homepage logout={this.logout}/>
);
}
}
So I made a Navigation.js page where I'm creating a drawernavigator and going to be returning this instead of the Homepage.
export default Navigation = createDrawerNavigator({
Home: {
screen: Homepage,
},
WebView: {
screen: WebView,
},
});
But I'm not sure how to pass along the state change from the homepage, through the Navigation component to the parent Authentication page. Any help would be much appreciated.
You could pass a callback through navigate:
this.props.navigation.navigate('yourTarget',{callback:function});
In yourTraget you can access it via:
this.props.navigation.state.params.callback(isAuthenticated)
Here the documentation: https://reactnavigation.org/docs/en/params.html
I hope this is what you were looking for! Oh now I see you asked that already a while ago. Maybe you already moved on...