How to update theme when device orientation changes - react-native

I am trying to implement orientation changing with hooks. I called the orientation hook from app.tsx and I want to update everything(theme,style in component) that uses widthPercentageToDP() function. How can I achieve this. I can't figured out.
useOrientation.tsx
export let { width, height } = Dimensions.get("window");
const heightPercentageToDP = (heightPercent: string | number): number => {
// Parse string percentage input and convert it to number.
const elemHeight =
typeof heightPercent === "number"
? heightPercent
: parseFloat(heightPercent);
// Use PixelRatio.roundToNearestPixel method in order to round the layout
// size (dp) to the nearest one that correspons to an integer number of pixels.
return PixelRatio.roundToNearestPixel((height * elemHeight) / 100);
};
export const useScreenDimensions = () => {
const [screenData, setScreenData] = useState({});
useEffect(() => {
setScreenData({orientation:currentOrientation()});
Dimensions.addEventListener("change", (newDimensions) => {
width = newDimensions.screen.width;
height = newDimensions.screen.height;
setScreenData({orientation:currentOrientation()}); // can be used with this height and width
//console.log(newDimensions.window);
});
return () => Dimensions.removeEventListener("change", () => {});
});
return {
width,height,
screenData
};
};
Theme file
const theme = {
spacing: {
m:widthPercentageToDP("2%") // it must be updated when orientation changes.
},
borderRadii: {
s:widthPercentageToDP("5%") // it must be updated when orientation changes.
},
textVariants: {
body:{
fontSize:widthPercentageToDP("%3"),
}
},
};
App.tsx
const {screenData} = useScreenDimensions();
console.log(screenData)
return (
<ThemeProvider>
<LoadAssets {...{ fonts, assets }}>
<Example/>
</LoadAssets>
</ThemeProvider>
);
}
Example.tsx
export const Example = ({}) => {
return (
<Box>
<Text variant="body">hey</Text>
{/* // it must be updated when orientation changes. */}
<View style={{width:widthPercentageToDP("40%")}}/>
</Box>
);
}
Box and theme come from theme.tsx file. Text component accepts variant prop that defined in theme.tsx

Using react-native-orientation you can do what you want, then the device orientation changes.
Example:
import Orientation from 'react-native-orientation';
export default class AppScreen extends Component {
componentWillMount() {
const initial = Orientation.getInitialOrientation();
if (initial === 'PORTRAIT') {
// do something
} else {
// do something else
}
},
componentDidMount() {
// this will listen for changes
Orientation.addOrientationListener(this._orientationDidChange);
},
_orientationDidChange = (orientation) => {
if (orientation === 'LANDSCAPE') {
// do something with landscape layout
} else {
// do something with portrait layout
}
},
componentWillUnmount() {
// Remember to remove listener to prevent memory leaks
Orientation.removeOrientationListener(this._orientationDidChange);
}

Related

How to call hook in class component in React Native?

Tech used
React Native Appearance, Typescript & Redux Rematch.
Problem
I am attempting to pass my customized theme colours into a class component. I also understand that hooks cannot be used/called within a class component and only a functional component. The reason why I am using a class component is because of Redux Rematch. Is there a way for me to get my colours from the hook listed below into my class component?
This is how I am building my theme:
index.tsx
const palette = {
colourTextDark: "#ffffff",
colourTextLight: "#000000",
};
export const colors = {
colourText: palette.colourTextLight,
};
export const themedColors = {
default: {
...colors,
},
light: {
...colors,
},
dark: {
...colors,
colourText: palette.colourTextDark,
},
};
hooks.tsx
import { useColorScheme } from "react-native-appearance";
import { themedColors } from "./";
export const useTheme = () => {
const theme = useColorScheme();
const colors = theme ? themedColors[theme] : themedColors.default;
return {
colors,
theme,
};
};
Ideally I would want to use it like so:
import { useTheme } from "../../theme/hooks";
...
class Example extends React.Component<Props> {
render() {
// This doesn't work
const { colors } = useTheme();
return (
<Text style={{ color: colors.colourText }}>Please help :)</Text>
)
}
}
How would I be able to do this? Thanks in advance :)
You could create a high order component like this:
const themeHOC = (Component) => {
return (WrappedComponent = (props) => {
const { colors } = useTheme();
return <Component {...props} colors={colors} />;
});
};
And use it like this:
themeHOC(<Example />)
This worked for me.
componentDidMount() {
(async () => {
const theme = useColorScheme() === "dark" ? styles.dark : styles.light;
this.setState({ theme });
})();
this.sendEmail();
}

react native setInterval cannot read property apply

I am new in react native I am trying to render the count of unread notification for that I called my API in HOC it is working fine for initial few seconds but after that, I started to get the below error
func.apply is not a function
below is my code
import React, { Component } from "react";
import PropTypes from "prop-types";
import { Modal, View } from "react-native";
import { themes } from "./constants";
import { AsyncStorage } from "react-native";
export default (OriginalComponent, animationType) =>
class extends Component {
static propTypes = {
handleFail: PropTypes.func,
theme: PropTypes.string,
visible: PropTypes.bool
};
state = {
modalVisible: true
};
static getDerivedStateFromProps({ visible }) {
if (typeof visible === "undefined") {
setInterval(
AsyncStorage.getItem("loginJWT").then(result => {
if (result !== null) {
result = JSON.parse(result);
fetch(serverUrl + "/api/getUnreadNotificationsCount", {
method: "GET",
headers: {
Authorization: "Bearer " + result.data.jwt
}
})
.then(e => e.json())
.then(function(response) {
if (response.status === "1") {
if (response.msg > 0) {
AsyncStorage.setItem(
"unreadNotification",
JSON.stringify(response.msg)
);
} else {
AsyncStorage.setItem("unreadNotification", 0);
}
}
})
.catch(error => {
alert(error);
// console.error(error, "ERRRRRORRR");
});
} else {
AsyncStorage.setItem("unreadNotification", 0);
}
}),
5000
);
return null;
}
return { modalVisible: visible };
}
handleOpenModal = () => {
this.setState({ modalVisible: true });
};
handleCloseModal = () => {
const { handleFail } = this.props;
this.setState({ modalVisible: false }, handleFail);
};
render() {
const { modalVisible } = this.state;
const { theme } = this.props;
return (
<View>
<Modal
animationType={animationType ? animationType : "fade"}
transparent={true}
visible={modalVisible}
onRequestClose={this.handleCloseModal}
>
<View style={themes[theme] ? themes[theme] : themes.transparent}>
<OriginalComponent
handleCloseModal={this.handleCloseModal}
{...this.props}
/>
</View>
</Modal>
</View>
);
}
};
I have not used getDerivedStateFromProps but, according to the docs, it is called on initial component mount and before each render update.
Thus your code is creating a new interval timer on each update without clearing any of the earlier timers, which could be causing a race condition of some sort.
You may want to consider using the simpler alternatives listed in the docs, or at a minimum, insure that you cancel an interval before creating a new one.

Confirm/warn dialog on back

Like in the web browser, we have onBeforeUnload (vs onUnload) to show a alert or some warning "there is unsaved data - are you sure you want to go back".
I am trying to do the same. I couldn't find anything in the docs of react-navigation.
I thought of doing something real hacky like this, but I don't know if its the right way:
import React, { Component } from 'react'
import { StackNavigator } from 'react-navigation'
export default function ConfirmBackStackNavigator(routes, options) {
const StackNav = StackNavigator(routes, options);
return class ConfirmBackStackNavigatorComponent extends Component {
static router = StackNav.router;
render() {
const { state, goBack } = this.props.navigation;
const nav = {
...this.props.navigation,
goBack: () => {
showConfirmDialog()
.then(didConfirm => didConfirm && goBack(state.key))
}
};
return ( <StackNav navigation = {nav} /> );
}
}
}
React navigation 5.7 has added support for it:
function EditText({ navigation }) {
const [text, setText] = React.useState('');
const hasUnsavedChanges = Boolean(text);
React.useEffect(
() =>
navigation.addListener('beforeRemove', (e) => {
if (!hasUnsavedChanges) {
// If we don't have unsaved changes, then we don't need to do anything
return;
}
// Prevent default behavior of leaving the screen
e.preventDefault();
// Prompt the user before leaving the screen
Alert.alert(
'Discard changes?',
'You have unsaved changes. Are you sure to discard them and leave the screen?',
[
{ text: "Don't leave", style: 'cancel', onPress: () => {} },
{
text: 'Discard',
style: 'destructive',
// If the user confirmed, then we dispatch the action we blocked earlier
// This will continue the action that had triggered the removal of the screen
onPress: () => navigation.dispatch(e.data.action),
},
]
);
}),
[navigation, hasUnsavedChanges]
);
return (
<TextInput
value={text}
placeholder="Type something…"
onChangeText={setText}
/>
);
}
Doc: https://reactnavigation.org/docs/preventing-going-back
On current screen set
this.props.navigation.setParams({
needUserConfirmation: true,
});
In your Stack
const defaultGetStateForAction = Stack.router.getStateForAction;
Stack.router.getStateForAction = (action, state) => {
if (state) {
const { routes, index } = state;
const route = get(routes, index);
const needUserConfirmation = get(route.params, 'needUserConfirmation');
if (
needUserConfirmation &&
['Navigation/BACK', 'Navigation/NAVIGATE'].includes(action.type)
) {
Alert.alert('', "there is unsaved data - are you sure you want to go back", [
{
text: 'Close',
onPress: () => {},
},
{
text: 'Confirm',
onPress: () => {
delete route.params.needUserConfirmation;
state.routes.splice(index, 1, route);
NavigationService.dispatch(action);
},
},
]);
// Returning null from getStateForAction means that the action
// has been handled/blocked, but there is not a new state
return null;
}
}
return defaultGetStateForAction(action, state);
};
Notes,
Navigating without the navigation prop
https://reactnavigation.org/docs/en/navigating-without-navigation-prop.html
NavigationService.js
function dispatch(...args) {
_navigator.dispatch(...args);
}
This can be accomplished by displaying a custom back button in the header, and capturing the hardware back-event before it bubbles up to the navigator.
We'll first configure our page to show a custom back button by overriding the navigation options:
import React, { Component } from 'react'
import { Button } from 'react-native'
function showConfirmDialog (onConfirmed) { /* ... */ }
class MyPage extends Component {
static navigationOptions ({ navigation }) {
const back = <Button title='Back' onPress={() => showConfirmDialog(() => navigation.goBack())} />
return { headerLeft: back }
}
// ...
}
The next step is to override the hardware back button. For that we'll use the package react-navigation-backhandler:
// ...
import { AndroidBackHandler } from 'react-navigation-backhandler'
class MyPage extends Component {
// ...
onHardwareBackButton = () => {
showConfirmDialog(() => this.props.navigation.goBack())
return true
}
render () {
return (
<AndroidBackHandler onBackPress={this.onHardwareBackButton}>
{/* ... */}
</AndroidBackHandler>
)
}
}

Scrolling issues with FlatList when rows are variable height

I'm using a FlatList where each row can be of different height (and may contain a mix of both text and zero or more images from a remote server).
I cannot use getItemLayout because I don't know the height of each row (nor the previous ones) to be able to calculate.
The problem I'm facing is that I cannot scroll to the end of the list (it jumps back few rows when I try) and I'm having issues when trying to use scrollToIndex (I'm guessing due to the fact I'm missing getItemLayout).
I wrote a sample project to demonstrate the problem:
import React, { Component } from 'react';
import { AppRegistry, StyleSheet, Text, View, Image, FlatList } from 'react-native';
import autobind from 'autobind-decorator';
const items = count => [...Array(count)].map((v, i) => ({
key: i,
index: i,
image: 'https://dummyimage.com/600x' + (((i % 4) + 1) * 50) + '/000/fff',
}));
class RemoteImage extends Component {
constructor(props) {
super(props);
this.state = {
style: { flex: 1, height: 0 },
};
}
componentDidMount() {
Image.getSize(this.props.src, (width, height) => {
this.image = { width, height };
this.onLayout();
});
}
#autobind
onLayout(event) {
if (event) {
this.layout = {
width: event.nativeEvent.layout.width,
height: event.nativeEvent.layout.height,
};
}
if (!this.layout || !this.image || !this.image.width)
return;
this.setState({
style: {
flex: 1,
height: Math.min(this.image.height,
Math.floor(this.layout.width * this.image.height / this.image.width)),
},
});
}
render() {
return (
<Image
onLayout={this.onLayout}
source={{ uri: this.props.src }}
style={this.state.style}
resizeMode='contain'
/>
);
}
}
class Row extends Component {
#autobind
onLayout({ nativeEvent }) {
let { index, item, onItemLayout } = this.props;
let height = Math.max(nativeEvent.layout.height, item.height || 0);
if (height != item.height)
onItemLayout(index, { height });
}
render() {
let { index, image } = this.props.item;
return (
<View style={[styles.row, this.props.style]}>
<Text>Header {index}</Text>
<RemoteImage src = { image } />
<Text>Footer {index}</Text>
</View>
);
}
}
export default class FlatListTest extends Component {
constructor(props) {
super(props);
this.state = { items: items(50) };
}
#autobind
renderItem({ item, index }) {
return <Row
item={item}
style={index&1 && styles.row_alternate || null}
onItemLayout={this.onItemLayout}
/>;
}
#autobind
onItemLayout(index, props) {
let items = [...this.state.items];
let item = { ...items[index], ...props };
items[index] = { ...item, key: [item.height, item.index].join('_') };
this.setState({ items });
}
render() {
return (
<FlatList
ref={ref => this.list = ref}
data={this.state.items}
renderItem={this.renderItem}
/>
);
}
}
const styles = StyleSheet.create({
row: {
padding: 5,
},
row_alternate: {
backgroundColor: '#bbbbbb',
},
});
AppRegistry.registerComponent('FlatListTest', () => FlatListTest);
Use scrollToOffset() instead:
export default class List extends React.PureComponent {
// Gets the total height of the elements that come before
// element with passed index
getOffsetByIndex(index) {
let offset = 0;
for (let i = 0; i < index; i += 1) {
const elementLayout = this._layouts[i];
if (elementLayout && elementLayout.height) {
offset += this._layouts[i].height;
}
}
return offset;
}
// Gets the comment object and if it is a comment
// is in the list, then scrolls to it
scrollToComment(comment) {
const { list } = this.props;
const commentIndex = list.findIndex(({ id }) => id === comment.id);
if (commentIndex !== -1) {
const offset = this.getOffsetByIndex(commentIndex);
this._flatList.current.scrollToOffset({ offset, animated: true });
}
}
// Fill the list of objects with element sizes
addToLayoutsMap(layout, index) {
this._layouts[index] = layout;
}
render() {
const { list } = this.props;
return (
<FlatList
data={list}
keyExtractor={item => item.id}
renderItem={({ item, index }) => {
return (
<View
onLayout={({ nativeEvent: { layout } }) => {
this.addToLayoutsMap(layout, index);
}}
>
<Comment id={item.id} />
</View>
);
}}
ref={this._flatList}
/>
);
}
}
When rendering, I get the size of each element of the list and write it into an array:
onLayout={({ nativeEvent: { layout } }) => this._layouts[index] = layout}
When it is necessary to scroll the screen to the element, I summarize the heights of all the elements in front of it and get the amount to which to scroll the screen (getOffsetByIndex method).
I use the scrollToOffset method:
this._flatList.current.scrollToOffset({ offset, animated: true });
(this._flatList is ref of FlatList)
So what I think you can do and what you already have the outlets for is to store a collection by the index of the rows layouts onLayout. You'll want to store the attributes that's returned by getItemLayout: {length: number, offset: number, index: number}.
Then when you implement getItemLayout which passes an index you can return the layout that you've stored. This should resolve the issues with scrollToIndex. Haven't tested this, but this seems like the right approach.
Have you tried scrollToEnd?
http://facebook.github.io/react-native/docs/flatlist.html#scrolltoend
As the documentation states, it may be janky without getItemLayout but for me it does work without it
I did not find any way to use getItemLayout when the rows have variable heights , So you can not use initialScrollIndex .
But I have a solution that may be a bit slow:
You can use scrollToIndex , but when your item is rendered . So you need initialNumToRender .
You have to wait for the item to be rendered and after use scrollToIndex so you can not use scrollToIndex in componentDidMount .
The only solution that comes to my mind is using scrollToIndex in onViewableItemsChanged . Take note of the example below :
In this example, we want to go to item this.props.index as soon as this component is run
constructor(props){
this.goToIndex = true;
}
render() {
return (
<FlatList
ref={component => {this.myFlatList = component;}}
data={data}
renderItem={({item})=>this._renderItem(item)}
keyExtractor={(item,index)=>index.toString()}
initialNumToRender={this.props.index+1}
onViewableItemsChanged={({ viewableItems }) => {
if (this.goToIndex){
this.goToIndex = false;
setTimeout(() => { this.myFlatList.scrollToIndex({index:this.props.index}); }, 10);
}
}}
/>
);
}
You can use onScrollToIndexFailed to avoid getItemLayout
onScrollToIndexFailed={info => {
const wait = new Promise(resolve => setTimeout(resolve, 100));
wait.then(() => {
refContainer.current?.scrollToIndex({
index: pinPosition || 0,
animated: true
});
});
}}

how to show/hide icons in a react native toolbar

I need to hide a hamburger-menu/location icon on the toolbar while the login screen is active. One option I thought would work is to have the icons set to a empty string by default. And use the EventEmitter in the success callback function in my Login.js & Logout.js, and then listen for it in my toolbar component. Sending a bool to determine show/hide. I am not sure if there is a better way of doing this so I'm up for suggestions. The Emit/Listen events work as expected. The issue is how I use a variable to apply the empty string or named icon.
here is the Toolbar Component.
export default class Toolbar extends Component {
//noinspection JSUnusedGlobalSymbols
static contextTypes = {
navigator: PropTypes.object
};
//noinspection JSUnusedGlobalSymbols
static propTypes = {
onIconPress: PropTypes.func.isRequired
};
//noinspection JSUnusedGlobalSymbols
constructor(props) {
super(props);
this.state = {
title: AppStore.getState().routeName,
theme: AppStore.getState().theme,
menuIcon: '',
locationIcon: ''
};
}
emitChangeMarket() {
AppEventEmitter.emit('onClickEnableNavigation');
}
//noinspection JSUnusedGlobalSymbols
componentDidMount = () => {
AppStore.listen(this.handleAppStore);
AppEventEmitter.addListener('showIcons', this.showIcons.bind(this));
};
//noinspection JSUnusedGlobalSymbols
componentWillUnmount() {
AppStore.unlisten(this.handleAppStore);
}
handleAppStore = (store) => {
this.setState({
title: store.routeName,
theme: store.theme
});
};
showIcons(val) {
if (val === true) {
this.setState({
menuIcon: 'menu',
locationIcon: 'location-on'
});
} else {
this.setState({
menuIcon: '',
locationIcon: ''
});
}
}
render() {
let menuIcon = this.state.menuIcon;
let locationIcon = this.state.locationIcon;
const {navigator} = this.context;
const {theme} = this.state;
const {onIconPress} = this.props;
return (
<MaterialToolbar
title={navigator && navigator.currentRoute ? navigator.currentRoute.title : 'Metro Tracker Login'}
primary={theme}
icon={navigator && navigator.isChild ? 'keyboard-backspace' : {menuIcon}}
onIconPress={() => navigator && navigator.isChild ? navigator.back() : onIconPress()}
actions={[{
icon: {locationIcon},
onPress: this.emitChangeMarket.bind(this)
}]}
rightIconStyle={{
margin: 10
}}
/>
);
}
}
The warning message I get is the:
Invalid prop icon of type object supplied to toolbar, expected a string.
how can I pass a string while wrapped in variable brackets?
Or if easier how can I hide/show the entire toolbar? either way works.
Try removing the brackets around menuIcon and locationIcon:
...
icon={navigator && navigator.isChild ? 'keyboard-backspace' : menuIcon}
...
icon: locationIcon,
...