Is it possible to navigate inside a Redux toolkit action to another screen after an asyncthunk is fullfilled? - react-native

So recently I am working on a react native app. I did wonder how I navigate from a fullfiled action?
I did not find away to be able to do that. What i have so far is this:
Dispatch Action
Navigate without knowing the result.
How is this achievable.
Some code:
Dispatch in the Submit function:
dispatch(
createTopic({
title: values.title,
subject: values.subject,
subsubject: values.subSubject,
description: values.description,
uid: userUid
})
, [dispatch])
export const createTopic = createAsyncThunk('topic/createTopic',
async ({title, subject, subsubject, description, uid}) =>{
try {
console.log(uid)
const response = await firebase.firestore().collection("Users").doc(uid).collection("Topics")
.doc()
.set({
title: title,
subject: subject,
subsubject: subsubject,
description: description
})
return response
} catch (error) {
console.log(error)
}
}
)

I'm assuming since this is react-native that you are using react-navigation? It is possible to navigate within the Redux action itself using the methods described in the docs page Navigating without the navigation prop. But it's probably better to initiate the navigation from the component.
The result of dispatching a thunk action is a Promise. Redux toolkit wraps this Promise so that there are no uncaught errors in your component. It always resolves to either a success or failure action. But you can unwrap() the result to use it with a try/catch.
const dispatch = useDispatch();
const navigation = useNavigation();
const handleSubmit = async (e) => {
try {
// wait for the action to complete successfully
const response = await dispatch(createTopic(args)).unwrap();
// then navigate
navigation.navigate(...);
} catch (error) {
// do something
}
}
Note: you should not use a try/catch in your createAsyncThunk. You want errors to be thrown so that they dispatch a 'topic/createTopic/rejected' action.

If you want to navigate from redux action and any were event when you don't have navigation prop, use ref of the navigation container,
eg:
export const navigationRef = createRef()
// when adding NavigationContainer add this ref
ie:
<Navigationcontainer ref={navigationRef}>
{...rest code}
</Navigationcontainer>
now you can use that ref to navigate to other screens
eg:
navigationRef.current?.navigate('ScreenName', params)
use above line the redux action...

Related

Using React-Navigation v5 with Redux & Firebase Authentication

I am trying to use React-Navigation Version 5 (not Ver 4) to switch from the authentication screens/stack to the non-authentication screens/stack when the user logs in. A simple example is well demonstrated in the documentation.
However, the documentation does not provide details for using Redux and Google Firebase authentication. After a successful login, I am getting the following warning message.
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
I think that the cause of this warning message is that the call to Redux action (action creator) causes re-rendering of the authentication screen, while the navigation code has already unmounted this screen! How can I update the Redux's store after the authentication screen has been unmounted?
Navigation Code:
...
// If user is logged in -> display the app/main stack
if (props.loggedIn)
{
const MyTabs = createBottomTabNavigator();
return (
<NavigationContainer>
<MyTabs.Navigator initialRouteName="UserTypeScreen">
<MyTabs.Screen name="UserTypeScreen" component={UserTypeScreen} />
<MyTabs.Screen name="PermissionsScreen" component={PermissionsScreen} />
</MyTabs.Navigator>
</NavigationContainer>
);
}
// If user is NOT logged in -> display the auth stack
else {
const AuthStack = createStackNavigator();
return (
<NavigationContainer>
<AuthStack.Navigator>
<AuthStack.Screen name="AuthScreen" component={AuthScreen} />
<AuthStack.Screen name="AuthLinkScreen" component={AuthLinkScreen} />
</AuthStack.Navigator>
</NavigationContainer>
);
}
...
Authentication screen:
...
useEffect( () => {
const unSubscribe = firebase.auth().onAuthStateChanged( (user) => {
if (user) {
const idToken = firebase.auth().currentUser.getIdToken();
const credential = firebase.auth.PhoneAuthProvider.credential(idToken);
// The following statement causes a warning message, because it causes re-rendering of an unmounted screen
props.acLoginUserSuccess(user, credential);
}
});
return () => {
unSubscribe();
};
}, []);
...
Redux Action Creator:
...
export const acLoginUserSuccess = (user, credential) => {
return {
type: LOGIN_USER_SUCCESS,
payload: { user: user, credential: credential }
};
};
...
I have quickly gone through you code, but here's what I think (could be wrong).
The docs recommend fetching the token on a screen which is mounted (App.js in the example).
This will not be an issue if your firebase.auth() listener in useEffect hook is in App.js .
I think the warning message is shown because when you are in MyTabs route, the AuthScreen is not mounted.
Moving the listener to App.js or on a screen which is in the same stack as BOTH the auth and MyTabs screen will solve the problem I think.
I believe that below code is written with idea to get token and store in Redux store.
Is it possible to move this logic after validation of credentials is successful instead of
unmount?
please provide valid reason of having this logic in unmount to assess the problem better
firebase.auth().onAuthStateChanged( (user) => {
if (user) {
const idToken = firebase.auth().currentUser.getIdToken();
const credential = firebase.auth.PhoneAuthProvider.credential(idToken);
// The following statement causes a warning message, because it causes re-rendering of an unmounted screen
props.acLoginUserSuccess(user, credential);
}
});

React-navigation how to navigate to a route within redux

I am new to react native with redux. I'm using react native + redux (with redux-thunk) + react-navigation.
I perform an operation within my redux action and intend to call a react-navigation route.
export const storeClient = (client) => {
return async dispatch => {
....
//does not work within action redux
//this.props.navigation.navigate('ListCliente');
}
}
There is information there in the react-navigation documentation:
Warning: in the next major version of React Navigation, to be released in Fall 2018, we will no longer provide any information about how to integrate with Redux and it may cease to work.
does not react-navigation offer a way to solve this problem? Can someone tell me this? How can I direct the route within the action using the react-navigation
EDIT
Is it a good practice to pass navigation as a parameter to use within redux?
my component
this.props.storeCliente(this.props.navigation, client)
my action (redux)
export const storeClient = (navigation, client) => {
return async dispatch => {
....
navigation.navigate('ListCliente');
}
}
** UPDATED
You need to :
1- import NavigationActions into your actions file (i.e. actions.js)
import { NavigationActions } from 'react-navigation'
2- Replace every "navigation" with NavigationActions, and your piece of code that calls the navigation, should looks like:
const loginUserSuccess = (dispatch, user, navigation) => {
dispatch({
type: LOGIN_USER_SUCCESS,
payload: user,
});
NavigationActions.reset({
index: 0,
actions: [NavigationActions.navigate({ routeName: 'employeeList' })],
}),
};

Issue with Unstated and React Navigation in React Native

I have the function
onPress = (store) => {
//store.flipState();
this.props.navigation.navigate('anotherScreen');
console.log('hi');
}
If I run it as above the navigation works.
If I uncomment the store.flipState() line the state changes but the navigation doesn't work (the screen just refreshes).
The console.log works in both cases.
How can I change the state and navigate at the same time?
I use Unstated and React Navigation in React Native.
Thank you.
I know this is really old, but what if you pass the navigate action to flipState
const {navigation: {navigate}} = this.props
store.flipState(navigate('anotherScreen'))
then, in flipState, when you call setState, pass the navigate as the success action callback
flipState = (callback) => {
this.setState((state) => {
return { flippedState: !state.flippedState };
}, callback);
};

React-Navigation: navigate from actions file

I'm new to RN and JS.
I want to navigate once a login action is complete, but I cannot get it to work. I am using firebase.
This is from my actions file. It throws a firebase error:
export const LOGIN_USER_SUCCESS = 'login_user_success';
export const loginUserSuccess = (dispatch, user) => {
dispatch({
type: LOGIN_USER_SUCCESS,
payload: user
});
this.props.navigation.navigate('groupMain'); // this doesnt work
};
I also tried putting it into the Login component - this might be a step in right direction because there is no firebase error, but nothing happens. So it seems that all works, except the user is not navigated to the proper screen. (I removed the 'this doesnt work' line from above)
componentWillReceiveProps(nextProps) {
this.onAuthComplete(nextProps);
}
onAuthComplete(props) {
if (props.user) {
this.props.navigation.navigate('groupMain');
}
}
Later, I also found this in the official docs and tried to implement by using this code in my action, but it threw a firebase error:
"dispatch - Send an action to the router
Use dispatch to send any navigation action to the router. The other navigation functions use dispatch behind the scenes.
Note that if you want to dispatch react-navigation actions you should use the action creators provided in this library.
See Navigation Actions Docs for a full list of available actions.
import { NavigationActions } from 'react-navigation'
const navigateAction = NavigationActions.navigate({
routeName: 'Profile',
params: {},
// navigate can have a nested navigate action that will be run inside the child router
action: NavigationActions.navigate({ routeName: 'SubProfileRoute'})
})
this.props.navigation.dispatch(navigateAction)"
thanks!
I solved a similar problem by creating a global service to handle programmatic navigation:
services/navigator.js
import { NavigationActions } from 'react-navigation';
let navigator;
export function setNavigator(nav) {
navigator = nav;
}
export function navigate(routeName, params) {
if (navigator) {
navigator.dispatch(NavigationActions.navigate({routeName, params}));
}
}
export function goBack() { ... }
export function reset() { ... }
Then, in my top-level component I store the reference to the navigator when it's created:
screens/App.js
import { setNavigator } from '../services/navigator';
const AppNavigator = StackNavigator(...);
class App extends Component {
render() {
return (
<AppNavigator ref={nav => setNavigator(nav)} />
);
}
}
And finally in my action files (or wherever I need to), I simply use the service to dispatch navigation actions:
actions/login.js
import { navigate } from '../services/navigator';
export function loginUserSuccess() {
// use navigate() anywhere you'd normally use this.props.navigation.navigate()
navigate('NextScreen');
}

Refresh Component on navigator.pop()

I'm using React Native's Navigator. Is there anyway to refresh the component so when I pop back to it, it'll make a new API call and grab the updated data to display in the component. I found a few similar questions, but no good answer...
Adding Api Call in callBack using a subscription. sovles the issue
componentDidMount() {
this.props.fetchData();
this.willFocusSubscription = this.props.navigation.addListener(
'willFocus',
() => {
this.props.fetchData();
}
);
}
componentWillUnmount() {
this.willFocusSubscription.remove();
}
You can send a callback function to nextscene from previous one as a prop.
this.props.navigator.push({
name: *nextscene*,
passProps: {
text: response,
callBack: this.callback
});
async callback(){
await ....//make new api request grab the udpated data
}
Then in your nextscene you call callback method and then pop. You can also send parameters
this.props.callBack()
this.props.navigator.pop()
When pop () to refresh before a page is not a good idea
You can try DeviceEventEmitter object
previous page DeviceEventEmitter.addListener('xxx', callback) in componentDidMount
current page DeviceEventEmitter.emit('xxx', anythingInCallback...) before pop()
ps:previous pageDeviceEventEmitter.removeAllListeners('xxx') in componentWillUnmount
I doubt you're still looking for an answer to this, but holy crap has this kept me up tonight. I'm very new to React Native, but I finally had some success.
The React Navigation API docs have a section for adding event listeners! Check it out! I shared some of my own code below too.
This is an example event handler in a Component that is the top screen of the StackNavigator stack. It grabs the current state and saves to the backend using an API call. After completion, StackNavigator's pop is called.
handleSubmit = () => {
const { value, otherValue } = this.state
addThingToDatabase({ value, otherValue })
.then(() => this.props.navigation.pop())
}
Now over to the other Component which is the screen "underneath" in the StackNavigator stack. This is screen being shown after the "pop". Here's what I used to have in ComponentDidMount.
componentDidMount() {
const { index } = this.props.navigation.state.params
getAllThingsFromDatabase({ index })
.then(({ arrayOfThings }) => this.setState({
index,
arrayOfThings
}))
}
But the Component wouldn't update with the new thing, until addListener! Now I have pretty much the same code except it's in the constructor. I figured I only need to run it one time, and I need to store it too.
constructor(props, context) {
super(props, context)
this.state = {
index: null,
arrayOfThings: []
}
this.willFocusSubscription = this.props.navigation.addListener(
'willFocus',
(payload) => {
const { index } = payload.state.params
getAllThingsFromDatabase({ index })
.then(({ arrayOfThings }) => this.setState({
index,
arrayOfThings
}))
}
)
}
Note that the docs also mention unsubscribing the event listener using the .remove() function. I put that in ComponentWillUnmount().
componentWillUnmount() {
this.willFocusSubscription.remove()
}
There are four different events to subscribe to. I went with willFocus thinking it'll update before the screen is seen.
You should save the state of the page and emit an action in componentDidMount since it is invoked immediately after a component is mounted.
References:
https://facebook.github.io/react/docs/react-component.html
https://github.com/ReactTraining/react-router
ADDED
Since your component has been already mounted you should listen ComonentWillReceiveProps instead.
The simple way is to use react native navigation resetTo function. It will replace the top item and pop to it.
If we do like this componentWillReceiveProps will call. So we can provide the API calls in that function and make it simple.
for more details https://facebook.github.io/react-native/docs/navigatorios.html#resetto