How to change react-router-native location on native-module callback - react-native

I'm building a react-native application that displays Service overlay (like those facebook messenger bubble heads), implement in Android only, and on the overlay click it should go back to the app in a specific screen.
I'm using react-router-native and I have my routes structured like this:
App.js
<NativeRouter>
<ApolloProvider client={client}>
<Main>
<Switch>
<Route exact path="/home" component={Home} />
<Route exact path="/progress" component={RouteProgress} />
<Route exact path="/search" component={Search} />
</Switch>
</Main>
</ApolloProvider>
</NativeRouter>
The Main component has these:
Main.js
componentDidMount() {
console.log(this.props.location.pathname);
if(this.props.location.pathname === '/') {
this.props.history.push("/home");
}
}
The callback from my Native module is being called like this:
FloatingView.java
case MotionEvent.ACTION_UP:
if (lastAction == MotionEvent.ACTION_DOWN || delta < 3) {
Intent intent = new Intent(FloatingWindow.this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
FloatingViewPackage.callback.invoke();
stopSelf();
}
The callback is defined in the component Search, which also executes the native module:
Search.js
<Button onPress={() => FloatingView.init(() => {
console.log('go to progress 1');
this.props.history.push("/progress");
setTimeout(() => {
console.log('go to progress 2');
this.props.history.push("/progress");
}, 1000);
})}
The problem is that this.props.history.push("/progress"); doesn't work neither outside the timeout nor inside.
Outside the timeout, the function is called before Main componentDidMount but location.pathname is not updated. Inside it the function is called after, but it doesn't navigate to the right screen. It always fall into /home.
I thought this my be a life cycle issue, since the Search component is not mounted. I've been trying to figure a way out to make this work. I tried using the Redirect component:
<Button onPress={() => FloatingView.init(() => <Redirect to="/progress" />)}
Does anyway can think of a way around this? Thanks

Found a solution, don't know if the best one, but it works.
Created a singleton navigation.js
navigation.js
export default {
entryPoint: '/home'
};
And changed the following files:
Search.js
<Button onPress={() => FloatingView.init(() => {
navigation.entryPoint = "/progress";
})}
Main.js
componentDidMount() {
if(this.props.location.pathname === '/') {
this.props.history.push(navigation.entryPoint);
}
}

Related

Trouble updating Picker component from Async Storage in React Native

I'm using a Picker component to let the user set a value for how frequently they want to be reminded about something. In the code below, I'm saving the result in the component's state as well as saving it to the device with Async Storage:
const [frequency, setFrequency] = useState('year');
...
<Picker
selectedValue={frequency}
style={styles.picker}
itemStyle={styles.pickerItem}
onValueChange={(itemValue, itemIndex) => {
(async () => {
await AsyncStorage.setItem(`#circle_${circle.name}_frequency`, itemValue)
})();
setFrequency(itemValue);
}}
mode={'dropdown'}
prompt={'Reminder every:'}
>
<Picker.Item label="Never" value="never" />
<Picker.Item label="Day" value="day" />
<Picker.Item label="Week" value="week" />
<Picker.Item label="Year" value="year" />
etc...
</Picker>
I'd also like to have the component grab the saved data and set that as the state when first rendering.
useEffect(() => {
const fetchFrequency = async () => {
let storedFrequency = await AsyncStorage.getItem(`#circle_${circle.name}_frequency`);
if (storedFrequency != null) {
setFrequency(storedFrequency);
};
}
fetchFrequency();
}, []);
Based on the limited amount I know about Async Storage, this seems to make sense. I think it's
awaiting the result of grabbing the value from storage
setting the state
rendering the component (this could be happening before setting state as well, but I figure it would render again when the state changes)
updating both storage and state when the user chooses a new option
However, this doesn't work. If I navigate away and then back to the page, the state has been reset.
UPDATE:
If I console.log the itemValue in the onValueChange async function this is what I get:
onValueChange={(itemValue, itemIndex) => {
(async () => {
await AsyncStorage.setItem(`#circle_${circle.name}_frequency`, itemValue)
console.log(itemValue)
})();
setFrequency(itemValue);
}}
When changing the value to 'never', it prints
never
never
When I navigate away and then come back, without even touching the compnent it prints out:
week
week
never
never
year
year
or
year
never
year
year
or some other long string of values which shows that there's a feedback loop going on somewhere.
Your expression AsyncStorage.setItem() is not firing because you forget to invoke Self-Invoking Functions inside the callback function of onValueChange.
onValueChange={(itemValue, itemIndex) => {
(async () => {
await AsyncStorage.setItem(`#circle_${circle.name}_frequency`, itemValue)
})(); // I will invoke myself
setFrequency(itemValue);
}}
UPDATED (following the updated question):
I didn't spot any more bugs on your given snippet code and I don't know what's going on with your full source code. Anyway, I have created a super simple working snippet code following by the code in your question, so you can just copy into your project.
import React, {useState, useEffect} from 'react';
import {Picker, AsyncStorage} from 'react-native';
export default function App() {
const [frequency, setFrequency] = useState('year');
useEffect(() => {
const fetchFrequency = async () => {
let storedFrequency = await AsyncStorage.getItem('#circle_circle_name_frequency');
if (storedFrequency != null) {
setFrequency(storedFrequency);
}
};
fetchFrequency();
}, []);
return (
<Picker
selectedValue={frequency}
onValueChange={(itemValue, itemIndex) => {
(async () => {
await AsyncStorage.setItem('#circle_circle_name_frequency', itemValue);
})();
setFrequency(itemValue);
}}
mode={'dropdown'}
prompt={'Reminder every:'}>
<Picker.Item label="Never" value="never" />
<Picker.Item label="Day" value="day" />
<Picker.Item label="Week" value="week" />
<Picker.Item label="Year" value="year" />
</Picker>
);
}
Hope this can help!
PS: I see you put expo tag on your question and I just wanna remind that, if you preview the project on the web browser, your storedFrequency inside useEffect will always be null because the browser doesn't support AsyncStorage.
It looks like the problem was an issue with the Picker itself and how it calls onValueChange every render rather than only when changed. I found a temporary solution in this thread for until it gets fixed: https://github.com/lawnstarter/react-native-picker-select/issues/112#issuecomment-634038287

Using React BrowserRouter (v4) how do I recognize when the location has changed?

Have the following JSX
// Root.jsx
<BrowserRouter getUserConfirmation={this.handleLocationChange}>
<Switch>
<Route path='/' exact component={Search} />
<Route path='/queue/' component={Queue} />
<Route path='/healthCheck/' component={HealthCheck} />
<Route path='/transcript' component={Transcript} />
</Switch>
</BrowserRouter>
// Root.js
export class Root extends Component{
constructor(props){
super(props);
}
handleLocationChange(message, callback) {
// your staff here
console.log(`- - - location:`);
callback(true);
}
render(){
return RootTemplate.call(this);
}
}
But when I run this I get...
Root.jsx:25 Uncaught TypeError: Cannot read property 'handleLocationChange' of undefined
at RootTemplate (Root.jsx:25)
at Root.render (Root.js:13)
If I try this...
getUserConfirmation={()=>this.handleLocationChange()}
I don't get an error but I also don't get my console like I would expect.
How do I tell when the location has changed?
Update
I also tried this just for testing...
const getConfirmation = (message, callback) => {
console.log("Welp this is working");
callback(true)
};
...
<BrowserRouter getUserConfirmation={getConfirmation}>
But still not seeing anything in the log.
If your trying to check when a user has navigated from one location in your app to another you should use the history library. You can also use it for getUserConfirmation, which is when a user navigates away from your application.
import createHistory from "history/createBrowserHistory";
import { Router } from "react-router";
// Fires when user navigates away
const getUserConfirmation = () => console.log("User navigated away");
// Create your browser history
const history = createHistory({getUserConfirmation});
// Fires every time a location is updated
history.listen((location) => console.log("Navigated to page", location));
// Attach your browser history to your router
const MyRouter = () => <Router history={history> {your children} </Router>
Use Prompt component from react-router. Include in one of you components.
Try doing this without using getUserConfirmation.
Prompt- https://reacttraining.com/react-router/core/api/Prompt

React to url change within component with React Router 4?

I have a component available at different urls. This is a simplified version of my app.js
<Router>
<Switch>
<Route path="/login"
render={() => {
return <SignUpAndRegister status="login" />;
}}
</Route>
<Route path="/register"
render={() => {
return <SignUpAndRegister status="register" />;
}}
</Route>
</Switch>
<Router>
I pass the status prop and then in the component setState based on the prop value. This works so far but I also need Links within the component that link to other other state. When your on the register page/state I have a link to login. When you're on the login page/state I have a link to register.
This means I have to have a link to change the url and also call a function to setState:
<Link
to="/login"
onClick={() => this.registerOrLogin('login')}
>
Log in
</Link>
How can I have the components state change when a url change is detected? It seems like browserHistory did this prior to v4 but I cant find an up up date solution.
I can set the state within the SignUpAndRegister component when it first mounts and then every time new props are received eg when the url changes.
componentDidMount() {
this.setState({ status: this.props.status });
}
componentWillReceiveProps(props) {
this.setState({ status: props.status });
}

Opening context menu on long press in React Native

I'd like to have a context menu triggered on long press different places using React Native.
I.e. in a dialer like the default dailer. You can long-click on any contact and get a 'copy number' menu. And also you can long-click on the name of the person once you've opened their 'contact card'.
The straight-forward way needs a lot of copy-pasted boilerplate, both components and handlers.
Is there a better pattern for doing this?
All Touchable components (TouchableWithoutFeedback, TouchableOpacity etc.) has a property called onLongPress. You can use this prop to listen for long presses and then show the context menu.
To eliminate code mess and doing lots of copy paste you can separate your context menu as a different component and call it when the long press happen. You can also use an ActionSheet library to show the desired options. React native has a native API for iOS called ActionSheetIOS. If you get a little bit more experience in react and react-native you can create a better logic for this but I'm going to try to give you an example below.
// file/that/contains/globally/used/functions.js
const openContextMenu = (event, user, callback) => {
ActionSheetIOS.showActionSheetWithOptions({
options: ['Copy Username', 'Call User', 'Add to favorites', 'Cancel'],
cancelButtonIndex: [3],
title: 'Hey',
message : 'What do you want to do now?'
}, (buttonIndexThatSelected) => {
// Do something with result
if(callback && typeof callback === 'function') callback();
});
};
export openContextMenu;
import { openContextMenu } from './file/that/contains/globally/used/functions';
export default class UserCard extends React.Component {
render() {
const { userObject } = this.props;
return(
<TouchableWithoutFeedback onLongPress={(event) => openContextMenu(event, userObject, () => console.log('Done')}>
<TouchableWithoutFeedback onLongPress={(event) => openContextMenu(event, userObject, () => console.log('Done'))}>
<Text>{userObject.name}</Text>
<Image source={{uri: userObject.profilePic }} />
</TouchableWithoutFeedback>
</TouchableWithoutFeedback>
);
}
}
Similarly as the previous answer combine onLongPress with imperative control for popup menu - something like
<TouchableWithoutFeedback onLongPress={()=>this.menu.open()}>
<View style={styles.card}>
<Text>My first contact name</Text>
<Menu ref={c => (this.menu = c)}>
<MenuTrigger text="..." />
<MenuOptions>
// ...
</MenuOptions>
</Menu>
</View>
</TouchableWithoutFeedback>
When it comes to a lot of boilerplate - in React you can do your own components that you can reuse everywhere thus reducing boilerplate (and copy&paste)
See full example on https://snack.expo.io/rJ5LBM-TZ

Detect what specific page is open?

Pretty simple question.. does anyone have a solution for detecting what component is open in React Native? Treating a page like a component?
My solution right now is to use a global state manager like Redux or Mobx and just constantly update it with whatever component you have open.
In you're renderScene property of your Navigator, you have access to the route object. So you can pass that down into whatever component you need.
<Navigator
configureScene={() => Navigator.SceneConfigs.FadeAndroid}
style={styles.navigator}
initialRoute={{ title: 'Welcome' }}
renderScene={(route, navigator) => {
if (route.title === Welcome) {
return <Welcome navigator={navigator} route={route} />
// Now inside Welcome, if you do this.props.route.title
// you can access the current route name 'Welcome'
}
}}
/>