So, I've been reacting for quite some time. I am facing a problem handling API and making it interact with small widgets in mobile app.
What I am trying to do ?
I am on a Screen(A) and on click of a button I go to Screen(B). Simple. Now in Screen(B) I fill up a form and submit it. While the form is submitting I have to show a Loading component. Finally the API responds if the request was "Success" or a "Failure".
If it was Success - Navigate back to Screen(A) and show a toast message(Boom..boom..) on screen(A).
If it Failed - Be in Screen(B) and show a toast message(yes, with a failure message).
My Approach
Let's start with reducers. I have following reducer state -
{
forSubmitRequest: false, // false - API has been trigerred to submit form
formSubmitRequestOver: true, // true - request is over
formSubmitRequestStatus: true // true - success
}
Now my actions are as follows -
case FORM_SUBMIT_REQUEST:
return {
...state,
formSubmitRequest: true,
formSubmitRequestOver: false,
formSubmitRequestStatus: false,
};
case FORM_SUBMIT_REQUEST_SUCCESS:
return {
...state,
formSubmitRequestOver: true,
formSubmitRequestStatus: true
};
case FORM_SUBMIT_REQUEST_FAILED:
return {
...state,
formSubmitRequestOver: true,
formSubmitRequestStatus: false,
};
case FORM_SUBMIT_REQUEST_DOWN:
return {
...state,
formSubmitRequest: false,
formSubmitRequestOver: true
};
Here's my coding logic in Screen(B)
const [formSubmitReq, setFormSubmitReq] = useState(false);
const [showErrorFormSubmitToast, setShowErrorFormSubmitToast] = useState(false);
useEffect(() => {
if (showErrorFormSubmitToast) {
Toast.show({
type: 'error',
text1: 'Error',
text2: 'Could not submit.',
topOffset: ResponsiveSize(0),
onHide: () => {
setShowErrorFormSubmitToast(false);
},
});
}
}, [showErrorFormSubmitToast]);
if (
formSubmitReq &&
props.teacher.formSubmitRequest &&
!props.teacher.formSubmitRequestOver
) {
return <Loading msg="Submitting form..." />;
}
if (
formSubmitReq &&
props.teacher.formSubmitRequest &&
props.teacher.formSubmitRequestOver
) {
if (props.teacher.formSubmitRequestStatus) {
props.navigation.goBack();
return <></>;
} else {
setFormSubmitReq(false);
setShowErrorFormSubmitToast(true);
props.handleCreateFormSubmitDown();
}
}
Logic in Screen(A)
const [showSuccessFormSubmitToast, setShowSuccessFormSubmitToast] =
useState(false);
useEffect(() => {
if (showSuccessFormSubmitToast) {
Toast.show({
type: 'success',
text1: 'Success',
text2: 'Successfully submitted.',
onHide: () => {
setShowSuccessFormSubmitToast(false);
},
});
}
}, [showSuccessFormSubmitToast]);
if (
!showSuccessFormSubmitToast &&
props.teacher.formSubmitRequest &&
props.teacher.formSubmitRequestOver &&
props.teacher.formSubmitRequestStatus
) {
console.log('Prep show toast');
setShowSuccessFormSubmitToast(true);
props.handleCreateFormSubmitDown();
}
Lastly this function - handleCreateFormSubmitDown just fires action - FORM_SUBMIT_REQUEST_DOWN
Gist of the code ->
I am trying to show Toast on Screen(A) only when request was success and request was actually fired. Not it may happen that request was success and I came back to Screen(A) now I simply navigate to Screen(B) so I have to make sure toast isn't visible as request wasn't fired. Now when reducer state updates then Screen(A) also gets rendered causing some nast things.
Can anyone point out what strategies they follow in such scenarios and where I could improve here?
One way to do this is to utilize route params from react-navigation
In screen B
...
useEffect(() => {
// Navigate to screen A with a route param
navigation.navigate(A, { displaySuccessToast: true });
}, [showSuccessFormSubmitToast])
In Screen A
...
useEffect(() => {
if (route.params.displaySuccessToast) {
Toast.show({
type: 'success',
text1: 'Success',
text2: 'Successfully submitted.',
onHide: () => {
// Hide Toast Action
},
});
}
}, [route.params]);
Other wise you can use a global state management solution like redux
Its the same logic. You need to update a flag in the global state. Then navigate to the earlier screen and check if this flag is set and render the toast message.
Related
I'm having a weird, issue, actually, I'm working with react-redux in a react-native app.
when the user logs in, everything goes smoothly in the user profile, but once I close the app and go back again, I'm getting a weird JSON format.
User reducer :
import AsyncStorage from '#react-native-async-storage/async-storage';
export function userReducer(
state = AsyncStorage.getItem('user') ? AsyncStorage.getItem('user') : null,
action,
) {
switch (action.type) {
case 'LOGIN':
return action.payload;
case 'LOGOUT':
return null;
case 'VERIFY':
return { ...state, verified: action.payload };
case 'USER_LIST_RESET':
return { users: [] };
default:
return state;
}
}
Login Form:
const submitForm = async () => {
if (isValidForm()) {
try {
const { data } = await axios.post(
'https://c36a-196-235-44-112.eu.ngrok.io/login',
{
email,
password,
},
);
dispatch({ type: 'LOGIN', payload: data });
AsyncStorage.setItem('user', JSON.stringify(data));
console.log('Logged In');
navigation.navigate('home');
} catch (error) {
console.log(error.message);
}
}
};
Getting the user in the userProfile :
const { user } = useSelector(state => ({ ...state }));
console.log(user)
The first Data I get once I login :
{"first_name": "John", "id": "63b0a010c015dd4b1f5fdb2c", "last_name": "Doe", "message": "Register Success ! please activate your email to start", "picture": "pic.png", "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYzYjBhMDEwYzAxNWRkNGIxZjVmZGIyYyIsImlhdCI6MTY3Mjk1MjU3NCwiZXhwIjoxNjczNTU3Mzc0fQ.XI9JnC4eeOQb5YJkNCC-Bqw4F9gpA1Xm6k_qJdkaXuw", "username": "JohnDoe", "verified": false}
The second data I get once I close and re open the app :
{"_A": null, "_x": 0, "_y": 1, "_z": "{\"id\":\"63b0a010c015dd4b1f5fdb2c\",\"username\":\"JohnDoe\",\"picture\":\"pic.png\",\"first_name\":\"John\",\"last_name\":\"Doe\",\"token\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYzYjBhMDEwYzAxNWRkNGIxZjVmZGIyYyIsImlhdCI6MTY3Mjk1MTY3NiwiZXhwIjoxNjczNTU2NDc2fQ.lVLg3pjfTFGt1K63v76sucinIUZJgOxSujSox12Xy0s\",\"verified\":false,\"message\":\"Register Success ! please activate your email to start\"}"}
I was trying to get the same data if I close the app, but I'm not, I'm actually getting another JSON form that is not compatible with the logic I'm working with.
AsyncStorage.getItem() returns a Promise, it is an asynchronous function:
https://reactnative.dev/docs/asyncstorage#getitem
So when retrieving an item you will either need to use async/await syntax to wait for the promise to resolve before you can do something with the retrieved item, or you will need to specify a callback function when calling AsyncStorage.getItem() in which you do something with the retrieved item.
i am able to generate notification along with action buttons but how to call a onpress event based on the button pressed by user? here yes or no
thanks in advance
import PushNotification from 'react-native-push-notification';
function configure() {
PushNotification.configure({
// (required) Called when a remote is received or opened, or local notification is opened
onNotification: function (notification) {
console.log('NOTIFICATION:', notification);
// process the notification
// (required) Called when a remote is received or opened, or local notification is opened
notification.finish();
},
// IOS ONLY (optional): default: all - Permissions to register.
permissions: {
alert: true,
badge: true,
sound: true,
},
// Should the initial notification be popped automatically
// default: true
popInitialNotification: true,
/**
* (optional) default: true
* - Specified if permissions (ios) and token (android and ios) will requested or not,
* - if not, you must call PushNotificationsHandler.requestPermissions() later
* - if you are not using remote notification or do not have Firebase installed, use this:
* requestPermissions: Platform.OS === 'ios'
*/
requestPermissions: true,
requestPermissions: Platform.OS === 'ios',
'content-available': 1,
});
}
function givenotification(title, message) {
PushNotification.localNotification({
channelId: 'channel',
message: message, // (required)
title: title,
message: message, // (required)
actions: ["Yes", "No"]
});
}
I have never used push notification directly,
for my notification i have use React Native Notifee that have this "displayNotification" inside when you try to log Notifee ..
I suggest you to try log "PushNotification" to find a function to display your notification.
You can setup the action as below code:
export const App = () => {
const [permissions, setPermissions] = useState({});
/**
* By calling this function, a notification with category `userAction` will have action buttons
*/
const setNotificationCategories = () => {
PushNotificationIOS.setNotificationCategories([
{
id: 'userAction',
actions: [
{id: 'open', title: 'Open', options: {foreground: true}},
{
id: 'ignore',
title: 'Desruptive',
options: {foreground: true, destructive: true},
},
{
id: 'text',
title: 'Text Input',
options: {foreground: true},
textInput: {buttonTitle: 'Send'},
},
],
},
]);
};
useEffect(() => {
PushNotificationIOS.addEventListener('notification', onRemoteNotification);
});
const onRemoteNotification = (notification) => {
const actionIdentifier = notification.getActionIdentifier();
if (actionIdentifier === 'open') {
// Perform action based on open action
}
if (actionIdentifier === 'text') {
// Text that of user input.
const userText = notification.getUserText();
// Perform action based on textinput action
}
};
};
For more, you can reach out to the official documentation of the library.
https://github.com/zo0r/react-native-push-notification
Scenario
When a user is authenticated (isAuthenticated booelan ref):
Check if a user has preferences by a graphql call to the backend (useViewerQuery)
If there are no preferences for the user set the default (useSetPreferenceDefaultMutation)
Problem
Both the query and the mutation work correctly in the graphql Playground and in the Vue app. They have been generated with the graphql codegenerator which uses useQuery and useMutation in the background.
The issue we're having is that we can't define the correct order. Sometimes useSetPreferenceDefaultMutation is executed before useViewerQuery. This resets the user's settings to the defaults and it not the desired behavior.
Also, on a page refresh all is working correctly. However, when closing an reopening the page it always calls useSetPreferenceDefaultMutation.
Code
export default defineComponent({
setup() {
const {
result: queryResult,
loading: queryLoading,
error: queryError,
} = useViewerQuery(() => ({
enabled: isAuthenticated.value,
}))
const {
mutate: setDefaultPreferences,
loading: mutationLoading,
error: mutationError,
called: mutationCalled,
} = useSetPreferenceDefaultMutation({
variables: {
language: 'en-us',
darkMode: false,
},
})
onMounted(() => {
watchEffect(() => {
if (
isAuthenticated.value &&
!queryLoading.value &&
!queryResult.value?.viewer?.preference &&
!mutationCalled.value
) {
void setDefaultPreferences()
}
})
})
return {
isAuthenticated,
loading: queryLoading || mutationLoading,
error: queryError || mutationError,
}
},
})
Failed efforts
We opened an issue here and here to have extra options on useQuery or useMutation which could help in our scenario but no luck.
Use fetch option with sync or post on watchEffect
Use watch instead of watchEffect
Thanks to comment from #xadm it's fixed now by using the onResult event hook on the query, so it will execute the mutation afterwards.
onResult(handler): Event hook called when a new result is available.
export default defineComponent({
setup(_, { root }) {
const {
loading: queryLoading,
error: queryError,
onResult: onQueryResult,
} = useViewerQuery(() => ({
enabled: isAuthenticated.value,
}))
const {
mutate: setDefaultPreferences,
loading: mutationLoading,
error: mutationError,
} = useSetPreferenceDefaultMutation({
variables: {
language: 'en-us',
darkMode: false,
},
})
onQueryResult((result) => {
if (!result.data.viewer.preference) {
void setDefaultPreferences()
}
})
return {
isAuthenticated,
loading: queryLoading || mutationLoading,
error: queryError || mutationError,
}
},
})
My problem is that when I go from one user page to another user page the info in component still remains from first user. So if I go from /user/username1 to /user/username2 info remains from username1. How can I fix this ? This is my code:
UserProfile.vue
mounted() {
this.$store.dispatch('getUserProfile').then(data => {
if(data.success = true) {
this.username = data.user.username;
this.positive = data.user.positiverep;
this.negative = data.user.negativerep;
this.createdAt = data.user.createdAt;
this.lastLogin = data.user.lastLogin;
data.invites.forEach(element => {
this.invites.push(element);
});
}
});
},
And this is from actions.js file to get user:
const getUserProfile = async ({
commit
}) => {
try {
const response = await API.get('/user/' + router.currentRoute.params.username);
if (response.status === 200 && response.data.user) {
const data = {
success: true,
user: response.data.user,
invites: response.data.invites
}
return data;
} else {
return console.log('Something went wrong.');
}
} catch (error) {
console.log(error);
}
};
Should I add watch maybe instead of mounted to keep track of username change in url ?
You can use watch with the immediate property, you can then remove the code in mounted as the watch handler will be called instead.
watch: {
'$route.params.username': {
handler: function() {
this.$store.dispatch('getUserProfile').then(data => {
if(data.success = true) {
this.username = data.user.username;
this.positive = data.user.positiverep;
this.negative = data.user.negativerep;
this.createdAt = data.user.createdAt;
this.lastLogin = data.user.lastLogin;
data.invites.forEach(element => {
this.invites.push(element);
});
}
});
},
deep: true,
immediate: true,
},
}
Your page is loaded before the data is retrieved it seems, you need put a "loading" property in the data and have a v-if="!loading" for your component then it will only render once the display is updated. Personally I would avoid watch if I can it is not great for performance of for fine grained handling.
Yes you should add wach on statement that contain user info.(you may have a problem to watch on object, so you can save user info in json, but im not sure). When user changing - call action, after recived response call mutation that should change a state, then watch this state.
And you might use better syntax to receive data from store. That is really bad idea call dispatch directly from your mouted hook, use vuex documentation to make your code better.
I'm building a React Native app and when one button is pressed I want to call two functions. The first one will make a get call and set the state loading: true, the second one will show a popup with the result of that get call.
I am calling the second function only if loading === false but it is executed immediately after the first one before the state can change, because loading is false by default. I can resolve this with setTimeout but I was wondering if there was a cleaner way to do this.
onPress() {
this.props.getUsers();
setTimeout(() => {
if (this.props.loading === false) {
this.props.popUpVisible();
}
}, 1000);
}
You can create callback function for that
getUsers = (callback) => {
//do whatever you want
//when it's done
callback();
}
In onPress function
onPress = () => {
this.props.getUsers(() => {
if (this.props.loading === false) {
this.props.popUpVisible();
}
});
}
setState Function can take two param:
setState(updater, callback)
setState({loading:true},() => {
//this fires once state.loading === true
})
Use getDerivedStateFromProps. It always fire when component's props change.
Below is the example.
class EmailInput extends Component {
state = {
email: this.props.defaultEmail,
prevPropsUserID: this.props.userID
};
static getDerivedStateFromProps(props, state) {
// Any time the current user changes,
// Reset any parts of state that are tied to that user.
// In this simple example, that's just the email.
if (props.userID !== state.prevPropsUserID) {
return {
prevPropsUserID: props.userID,
email: props.defaultEmail
};
}
return null;
}
// ...
}