React-Native: Re-render App on state change (change in authentication state) - authentication

Currently, I'm checking if the user is authorized during app start and based on the result, I display the app.
However, the user may sign in or sign out while using the app and I want to re-render the app in those cases so that my logic stays at one place (ie at app start).
Is there a way to achieve that?

By default, when an state changes, render function will call again. you can change render function based on an state and then when that state changed, the view has changes... for example:
render() {
return (
<View>
{
this.state.authorized == true ?
<Text>authorized</Text>
:
<Text>unauthorized!</Text>
}
</View>
)
}
so when this.state.authorized changes, render function shows different view based on state.
But if you want to restart the app on state change, you can use React Native Restart package and restart app when you want. There is no way to restart your app on every state change because there is many state changes in RN lifecycle and your app will restart frequently. I hope it will help you.

render() {
const { authorised } = this.state;
const userMessage = authorised ? "authorized" : "unauthorized";
return (
<View>
<Text>{ userMessage }</Text>
</View>
)
}
You can also use props to maintain the user's status and render the components/messages based on the props values.

Related

React Native (Expo) - Ability to update state of top component from any (nested) child component

I am working on an Expo app, that uses authentication with JWTs that are stored using SecureStore. The top-level component initially displays a Login screen but also checks if a JWT exists in SecureStore. If a JWT exists the app verifies that it is still valid and if it is the app takes the user to the Landing page, from which the user can navigate to many other pages that fetch and display all sorts of data.
I am looking for a way to handle an expired JWT, so that if the user is navigating to a page that tries to fetch some data and the API response returns e.g. 401 the app should take the user back to the login screen.
The top component uses this state to decide what page to show
const [appState, setAppState] = useState(appStates.STARTUP_SETUP);
with valid values for appState being:
const appStates = {
STARTUP_SETUP: "StartupSetup", // Initial state, during which the app checks for an existing valid JWT
SHOW_LOGIN_SCREEN: "ShowLoginScreen", // There is no stored JWT, app shows login screen
SHOW_SIGNUP_SCREEN: "ShowSignupScreen", // There is no stored JWT, app shows signup screen
SHOW_SIGNUP_CONFIRMATION_SCREEN: "ShowSignupConfirmationScreen", // There is no stored JWT, user just registered and is prompted to check their email for verification
USER_LOGGED_IN: "UserLoggedIn", // user logged in, JWT is stored
}
The component uses appState in the following way:
if (appState === appStates.USER_LOGGED_IN) {
comp = <Landing onLogout={logUserOut} />;
} else if (appState === appStates.SHOW_LOGIN_SCREEN) {
comp = <Login onSuccessfulLogin={updateUser} onSignup={() => setAppState(appStates.SHOW_SIGNUP_SCREEN)} />;
} else if (appState === appStates.SHOW_SIGNUP_SCREEN) {
comp = <Signup onSuccessfulSignup={() => setAppState(appStates.SHOW_SIGNUP_CONFIRMATION_SCREEN)} onLogin={() => setAppState(appStates.SHOW_LOGIN_SCREEN)} />;
} else if (appState === appStates.SHOW_SIGNUP_CONFIRMATION_SCREEN) {
comp = <SignupConfirmation onLogin={() => setAppState(appStates.SHOW_LOGIN_SCREEN)} />
}
With Landing having its own tree of child components.
I am basically looking for a way to be able to do
setAppState(appStates.SHOW_LOGIN_SCREEN)
from anywhere in my app.
One possibility would be to pass that hook from the top component to Landing and every child that has but I feel there should be an easier way.
Edit - Solution
In my top component I created a method that deletes the token and sets the appState to SHOW_LOGIN_SCREEN
const deleteStoredToken = () => {
deleteToken();
setAppState(appStates.SHOW_LOGIN_SCREEN);
}
const appStateValue = { deleteStoredToken };
I then created a context (with a default value)
export const AppContext = React.createContext({
deleteStoredToken: () => { }
});
I used this context to wrap the children of the top component by providing appStateValue as its value
return (
...
<AppContext.Provider value={appStateValue}>
{children}
</AppContext.Provider>
...
)
And now in any child component I can do
const { deleteStoredToken } = useContext(AppContext);
and use deleteStoredToken()
It sounds like you're looking for a state management solution. A React Context is a low-effort solution to this - be sure to read the caveats in the documentation though.
There are tens if not hundreds of libraries that offer different ways to do this. The most popular are probably Redux and Mobx; these offer dedicated ways to share complex state between components. However, if it's only for one value, and it won't be updated frequently, a Context is perfectly fine.

How can we prevent mobile app crashes if cached data does not align with new reducers structure?

I am currently developing an e-commerce management mobile application with React Native, and Redux. We persist data using redux-persist. This brings a great experience with cached data which is a principle in offline-first development.
But there might be a bug that can happen in the mobile world environment.
Let's assume that I have a reducer called "products". That reducer is just an array with product objects. The user logs in and now the data in that reducer is persisted. Later, my development team decides to update the mobile app with a new structure on that "products" reducer. The user's app gets updated, and now the persisted/cached data doesn't align with the new "products" reducer structure which leads to the app crashing.
I may be wrong, but is this an actual bug that can exist? If so, what is a work around or solution?
Thanks!
This is a potential bug that can exist i agree because it has happened to me twice and i discovered something which might be helpfull to you. The cause of the crash is dependent on what is stored on disk and what your code uses let me explain.
//let say we have a reducer
function ProductReducer(
state={
products:[],
fetching:false
},action){
switch(){......}
}
// lets say we have a combine reducer
combineReducers({
products: ProductReducer
})
//let say we have a component that consums this products
function RenderProducts(){
const {products, fetching} = useSelector((store)=>({
products: store.products.products,
fetching: store.products.fetching,
}))
return (
<View>
{
fetching && (
<Text>
loading...
</Text>
)
}
{products.map((item)=>
<View>
<Text>
{item.name}
</Text>
</View>)
}
</View>
)
}
NOW OUR UPDATE LOOKS LIKE THIS
// Now lets new reducer be
function ProductReducer(
state={
productsList:[],
fetchingProduct:false
},action){
switch(){......}
}
// Now our Render component becomes
function RenderProducts(){
const {products, fetching} = useSelector((store)=>({
products: store.products.productsList,
fetching: store.products.fetchingProduct,
}))
return (
<View>
{
fetching && (
<Text>
loading...
</Text>
)
}
{products.map((item)=>
<View>
<Text>
{item.name}
</Text>
</View>)
}
</View>
)
}
Now the second code is going to cause a crash crash for one reason you will be trying to call .map on an undefined object, why is this happening the simple reason is illustrated below
// Intial store
const store = {
products:{
products:[], // array of products
fetching: false,
}
}
**This was our store stored on disk as JSON string
**
AFTER OUR UPDATE TO REDUCER our store remains the same and before we fetch data from our server so that our reducer can write our update to disk the render product component is trying to apply our update and thus the crash
SOLUTION
You can purge the store // you will not want to this because all stored data including tokens and vital inofrmaton will be lost and user will start app afresh
Understanding this problem you can now have several work arounds like dispatching an action to enforce your migration on disk before your components get rendered . this is what i mean
to apply update i will do this ...
function ProductReducer(
state={
productsList:[],
fetchingProduct:false
},action){
switch(action.type){
case 'MIGRATING':{
if(store.products){ // checking if products exist on the products
reducer this will handle updates and new installs
state = {...state,productsList:
state.products,fetchingProduct:state.fetching}
}
return state;
}
}
}
This will write changes to disk but make sure this runs before your components are rendered i will do this for all reducers
I will just write my component to always check if a data they are reading is available or else render empty data like '',[],{} for string, array, and object respectively

Passing data from one screen to another React-native

I'm trying to pass data from one screen to another.
My problem is that the first screen goes to a calendar, when I've finished to put the dates in the calendar, I submit and go back to the first screen, the problem is that the first screen doesn't reload when I go back to it, so the data is not loaded, I think.
First screen : "Mytrips" = I click and go to Calendar
Calendar : I send the 2 datas I need with onPress
<TouchableOpacity
style={styles.touchable2}
onPress={() => this.props.navigation.navigate("MyTrips",
{selectedStartDate: selectedStartDate,
selectedEndDate: selectedEndDate })}
></TouchableOpacity>
So now, I go back to MyTrips and I set up the data recovery in the state :
this.state = {
selectedStartDate: props.navigation.state.params && props.navigation.state.params.selectedStartDate
? props.navigation.state.params.selectedStartDate
: "",
selectedEndDate: props.navigation.state.params && props.navigation.state.params.selectedEndDate
? props.navigation.state.params.selectedEndDate
: "",
and I want to display it in the screen :
<Text>{this.state.selectedStartDate}{this.state.selectedEndDate}</Text>
But, there is nothing displayed. I think that it's because when I go back to the first screen, the state is not updated with the data I passed.
Is anyone that can help me ? I think I'm close to get it work but ... I miss something. Thanks for any help
Ciao, this happens bacause this.state.selectedStartDate and this.state.selectedEndDate will be not updated when Calendar component changes selectedStartDate and selectedEndDate. In this.state = {... you are just copying props into state on first component loading. To update state with new values I suggest you to use componentDidUpdate in MyTrips component. Something like:
componentDidUpdate(prevProps, prevState) {
// here you could write:
// if (prevProps !== this.props) {
// this.setState with this.props
// }
}

How to render new image when available in react-native

I have a situation right now with my RN app, and I don't know how to tackle it.
I'm logging the user in through Facebook API, in our backend, we handle all the FB user data, and also, its profile picture that we crop to a certain size for performance purposes.
To do this, we run an async worker that will do the cropping, in the meantime, in the app we show a default user avatar with the app logo. But once the worker finished the task, the image isn't updated, not until I re-render the view, this re-render causes to run again "renderUserAvatar()" function which validates if the user has a profile picture or not. Which makes sense.
Now here's the help, how can I listen to this URL availability? So that whenever the Image is available, it re-renders?
At first, I thought about adding something like handling the Image's onError, by setting a setInterval, and trying to force a re-render, but that doesn't look very performant it rather sounds ugly.
Is there a clean way to handle this specific case-scenario?
This is my current renderUserAvatar function:
renderUserAvatar() {
const { userInfo } = this.props;
if (!_.isEmpty(userInfo) && userInfo.userPictures && userInfo.userPictures.length) {
const avatar = userInfo.userPictures.filter(pic => pic.isAvatar && pic.isEnabled);
if (avatar && avatar.length) {
const url = `${avatar[0].url}?height=${USER_AVATAR_HEIGHT}&width=${USER_AVATAR_WIDTH}`;
return <Thumbnail large style={ styles.userProfilePic } source={{uri: url}}/>;
}
}
return <Thumbnail large style={ styles.userProfilePic } source={ImageAssets['user-avatar']}/>
}
(Thumbnail is a NativeBase's component based on React-Native Image. So it would have all the Image methods and props too)
You could put your image into a state-variable.
At the first load (or if User-Image are not yet fetched) it is your Placeholder-Image.
If the async function runs on entering the screen and the images was fetched, replace the placeholder-image in the state with your new one.
The change of a state-variable will cause a re-render and this way, you'r image should appear.

How do I detect if a language has been changed when using the goBack() in React Native?

I have 3 pages that will be interacting with each other. a Login page, a Settings page, and a HomeScreen page. The login page contains a toolbar which has a clickable back arrow image(uses goBack()) and a clickable settings image which redirects to a settings page where a language can be picked.
There is no problem detecting a language change on the settings page because state is updated upon a change in language. However, if the user taps the backarrow image, the login page does NOT detect a change in state. How do I make sure that the login page can detect if the language has been changed(on the Settings page)?
I found on question that is similar to my problem here however, the answer provided uses navigate, whereas I'm using goBack. I know they're similar, but I'm unsure as to how/where I could pass a callback function on my settings page, and where to use refresh() on my Login page.
I use this method on my Login page
componentWillMount() {
AsyncStorage.getItem('language', (err, result) => {
langCode = result;
this.setState({
currLang: result,
})
});
}
On my Settings page:
onLangChange(value, index){
Config.langCode = value;
this.setState({ currLang: Config.langCode });
AsyncStorage.setItem('language', value)
console.log( 'here is your new lang ' + Config.langCode);
}
and then
render(){
const {navigate} = this.props.navigation;
const {goBack} = this.props.navigation
const langText = Config.langText[this.state.currLang]; // Object that has text in current language.
return(
<ScrollView style={styles.mainContainer}>
<View style={styles.headerContainer}>
<View style={styles.iconContainer}>
<TouchableOpacity onPress={ () => goBack('') } >
<Image source={Config.images.leftArrowIcon} style={styles.leftArrowIcon} />
</TouchableOpacity>
Use redux for global states like language in your example. It's just much simpler. If you have like 30 screens in your app, and every screen must display texts in correct language, this means that you have to send language back and forth between 30 screens, that's just horrible. redux is a state container, it's strongly recommended that you put those global states (states that many screens share) in redux and send them to each screen. If you don't know how to use redux yet, don't worry, it's not hard to understand, there're plenty good tutorials online. Here is the LanguagePickerExample I wrote for this question using redux, simply
npm install
or
yarn
then
react-native link
The result looks like this: