React Native error when using dynamic styling in combination with useEffect - javascript

The error I receive is:
[Invariant Violation: [369,"RCTImageView",31,{"source":[{"__packager_asset":true,"width":52,"height":80,"uri":"...assets/assets/glass.png?platform=ios&hash=b11854c2a0e54026f26338d7c7cbf7fc?platform=ios&dev=true&hot=false&minify=false","scale":1}],"width":"<>","height":"<>","overflow":"hidden","marginTop":4,"marginHorizontal":"<>","resizeMode":"contain"}] is not usable as a native method argument. It appears that the width, height and marginHorizontal are not available on render - I calculate these values based on screen size and the error disappears when I remove the dynamic styling. This error only happens when I use useEffect. I make the dynamic styling based on the width of a container:
<View
style={styles.container}
onLayout={(event) => {
setContainerWidth(event.nativeEvent.layout.width);
}}
>
{glasses.map(() => glassContainer())}
</View>
This is the component which uses the dynamic styling.
const ratio = 78 / 50;
const width = containerWidth / 12;
const glassContainer = () => (
<View style={styles.singleGlassContainer}>
<Image
source={require("../../assets/glass.png")}
style={[
styles.glass,
{
width: width,
height: width * ratio,
marginHorizontal: containerWidth / 72,
},
]}
resizeMode="contain"
/>
</View>
);
Then finally I use useEffect like below
useEffect(() => {
renderGlasses();
}, [hydrationTarget]);
The renderGlasses function:
const renderGlasses = () => {
//Calculates amounth of glasses to reach target
const targetGlasses = Math.ceil(hydrationTarget / glassVolume);
for (let i = 0; i < targetGlasses; i++) {
setGlasses((prevArray) => [...prevArray, i]);
}
};
I think what goes wrong is useEffect fires before the dynamic styling is calculated, but I might be wrong here. Anyone knows what is causing the error, and how to fix it? If any additional code is needed please let me know.

Related

How to change the speed of the color switch when button is clicked?

I made this code below that when you click the button the colors change. But I don't want the color change to happen instantly I want it to slowly change from one color to the next, like should take 5 seconds.
export default function App() {
const [colorIsRed,setColorIsRed] = React.useState(false);
return (
<View style={{flex:1,justifyContent:"center",alignItems:"center",backgroundColor:"black"}}>
<TouchableOpacity
style={{
width:100,
height:100,
borderRadius:20,
backgroundColor:colorIsRed?"red":"white",
borderWidth:1,
borderColor:colorIsRed?"white":"red",
}}
onPress={()=>{setColorIsRed(!colorIsRed)}}>
</TouchableOpacity>
</View>
);
}
What you need to do is take advantage of the Animated library in react-native. Here's a full example (https://snack.expo.dev/hXW2jw7yN). And below is the code explained with code comments.
export default function App() {
/**
* This is the animated value that keeps track of the
* progess of the animation
*/
const progress = React.useRef(new Animated.Value(0)).current;
/**
* Function to be called on button press
*/
const onPress = ()=>{
/**
* Runs a timing animation that moves the progress value to 1 or 0
* in 3 seconds
*/
Animated.timing(progress,{
toValue:progress._value===1?0:1, // If progress is 1 then set to 0 else set to 1
duration:3000, // Five seconds
}).start()
}
/**
* Uses the progess value to interpolate the color,
* this will "slowly change from one color to the next".
* Please search interpolate online and learn more
*
*/
const bgColor = progress.interpolate({
inputRange:[0,1],
outputRange:["white","red"]
})
const borderColor = progress.interpolate({
inputRange:[0,1],
outputRange:["red","white"]
})
return (
<View style={{flex:1,justifyContent:"center",alignItems:"center",backgroundColor:"black"}}>
<TouchableOpacity
onPress={onPress}>
<Animated.View style={{
width:100,
height:100,
borderRadius:20,
backgroundColor:bgColor,
borderColor:borderColor
}}>
</Animated.View>
</TouchableOpacity>
</View>
);
}
This may be silly, but have you tried adding:
transitionDuration: 5s
to style attribute?

How to detect screen orientation change in react-native app with a funtion component

I'm making a calculator App for react-native and I want to render different layout for buttons in landscape and portrait mode, how can I do it in a function component
my return:
return (
<View style={styles.container}>
<View style={styles.resultContainer}>
<Text style={styles.resultText}>
{displayValue}
</Text>
</View>
<View style={styles.inputContainer } >
{renderButtons()}
</View>
</View>
);
and my RenderButtons funtion
renderButtons = () =>{
height = Dimensions.get('window').height
width = Dimensions.get('window').width
if(height > width){
let layouts = buttons.map((buttonRows,index)=>{
let rowItem = buttonRows.map((buttonItems,buttonIndex)=>{
return <InputButton
value={buttonItems}
handleOnPress={handleInput.bind(buttonItems)}
key={'btn-'+ buttonIndex}/>
});
return <View style={styles.inputRow} key={'row-' + index}>{rowItem}</View>
});
return layouts
}
else
{
let layouts = buttons_landscape.map((buttonRows,index)=>{
let rowItem = buttonRows.map((buttonItems,buttonIndex)=>{
return <InputButton
value={buttonItems}
handleOnPress={handleInput.bind(buttonItems)}
key={'btn-'+ buttonIndex}/>
});
return <View style={styles.inputRow} key={'row-' + index}>{rowItem}</View>
});
return layouts
}
}
of course (height > width) condition doesn't work because apparently the values are undefined, I'm kinda new to JS so please don't be to harsh on me for not knowing the obvious
You can use react-native-orientation-locker
import Orientation from 'react-native-orientation-locker';
_onOrientationDidChange = (orientation) => {
if (orientation == 'LANDSCAPE-LEFT') {
//do something with landscape left layout
} else {
//do something with portrait layout
}
};
componentWillMount() {
//The getOrientation method is async. It happens sometimes that
//you need the orientation at the moment the js starts running on device.
//getInitialOrientation returns directly because its a constant set at the
//beginning of the js code.
var initial = Orientation.getInitialOrientation();
if (initial === 'PORTRAIT') {
//do stuff
} else {
//do other stuff
}
},
componentDidMount() {
Orientation.getAutoRotateState((rotationLock) => this.setState({rotationLock}));
//this allows to check if the system autolock is enabled or not.
Orientation.lockToPortrait(); //this will lock the view to Portrait
//Orientation.lockToLandscapeLeft(); //this will lock the view to Landscape
//Orientation.unlockAllOrientations(); //this will unlock the view to all Orientations
//get current UI orientation
/*
Orientation.getOrientation((orientation)=> {
console.log("Current UI Orientation: ", orientation);
});
//get current device orientation
Orientation.getDeviceOrientation((deviceOrientation)=> {
console.log("Current Device Orientation: ", deviceOrientation);
});
*/
Orientation.addOrientationListener(this._onOrientationDidChange);
},
componentWillUnmount: function() {
Orientation.removeOrientationListener(this._onOrientationDidChange);
}
Another approach can be Here
You can use this library react-native-orientation
Example:
import Orientation from 'react-native-orientation';
const AppScreen = () => {
// ...
const [deviceOrientation, setDeviceOrientation] = React.useState('');
React.useEffect(() => {
// The getOrientation method is async. It happens sometimes that
// you need the orientation at the moment the JS runtime starts running on device.
// `getInitialOrientation` returns directly because its a constant set at the
// beginning of the JS runtime.
// this locks the view to Portrait Mode
Orientation.lockToPortrait();
// this locks the view to Landscape Mode
// Orientation.lockToLandscape();
// this unlocks any previous locks to all Orientations
// Orientation.unlockAllOrientations();
Orientation.addDeviceOrientationListener(handleChangeOrientation);
return () => {
Orientation.removeAllListeners(handleChangeOrientation);
console.log('Bye see you soon ! 👋');
};
}, []);
const handleChangeOrientation = orientation => {
console.log(' Device orientation change to:', orientation);
setDeviceOrientation(orientation);
};
return (
<View style={{flex: 1}}>
<Text>{deviceOrientation}</Text>
</View>
);
};
export default AppScreen;

Framer-motion drag not respecting previously updated props

A simple use-case is to allow a user to either click buttons to paginate in a slider, or drag. Both events call the same paginate function with a param to either go forward or back--simple stuff.
However, the trigger from drag seems to cause bizarre behavior where the slider wants to start the animation from several slides back as if it ignores the updated props. This doesn't happen when using the buttons and both use the same simple paginate call.
Any tips appreciated.
Minimal example:
export default function App() {
const [position, setPosition] = useState<number>(0);
const paginate = (direction: Direction) => {
setPosition((prev) => {
return direction === Direction.Forward
? Math.max(-800, prev - 200)
: Math.min(0, prev + 200);
});
};
return (
<div className="App">
<Slider>
<Wrapper
animate={{ x: position }}
transition={{
x: { duration: 1, type: "tween" }
}}
drag="x"
dragConstraints={{
top: 0,
left: 0,
right: 0,
bottom: 0
}}
onDragEnd={(e, { offset, velocity }) => {
const swipe = swipePower(offset.x, velocity.x);
if (swipe < -swipeConfidenceThreshold) {
paginate(Direction.Forward);
} else if (swipe > swipeConfidenceThreshold) {
paginate(Direction.Back);
}
}}
>
<Slide>1</Slide>
<Slide className="alt">2</Slide>
<Slide>3</Slide>
<Slide className="alt">4</Slide>
<Slide>5</Slide>
</Wrapper>
</Slider>
<button onClick={() => paginate(Direction.Back)}>prev</button>
<button onClick={() => paginate(Direction.Forward)}>next</button>
</div>
);
}
Codesandbox Demo
I have to say, this problem is quite interesting. However, I think I figured out a way for you to handle this. One thing I noticed is that if you comment out
onDragEnd={(e, { offset, velocity }) => {
// const swipe = swipePower(offset.x, velocity.x);
// if (swipe < -swipeConfidenceThreshold) {
// paginate(Direction.Forward);
// } else if (swipe > swipeConfidenceThreshold) {
// paginate(Direction.Back);
// }
}}
the entire onDragEnd prop function, this example still doesn't work, since by the looks of things, the draggable component is not respecting your offset.
I realized that at this point, the problem is the internal state of the component is out of sync with your state. And would you look at that, the Framer Motion API actually provides a way to inspect this.
https://www.framer.com/api/motion/motionvalue/#usemotionvalue
It's the hook useMotionValue() which allows us to see what's actually happening. Turns out, our value is being set wrong when the user starts dragging:
useEffect(
() =>
motionX.onChange((latest) => {
console.log("LATEST: ", latest);
}),
[]
);
We can see this, because the state "jumps" to 200 as soon as we start dragging.
So fixing in theory is easy, we just need to make sure to let that value "know" about our offset, and that way it's gonna start with the proper offset in mind!
Anyway, that was my thought process, here's the solution, all you need to do is set the left constraint to make it work:
dragConstraints={{
top: 0,
left: position,
right: 0,
bottom: 0
}}
And tada! This makes it work. Here's my working solution: https://codesandbox.io/s/lingering-waterfall-2tsfi?file=/src/App.tsx

Use a slider to scroll a horizontal scrollView in ReactNative

I am trying to set up a slider inside a horizontal ScrollView that would allow me to scroll the page faster. I am able to link position of the page to the value of the slider, so that when I scroll the page, the thumb of the slider moves accordingly.
I am using React Native Slider and a ScrollView.
Here is the result that I am unfortunately having.
I am quite new to RN, so I am probably missing something important here.
class Comp extends Component {
state = {
width : 0,
value : 0,
cursor : 0
}
moveTheCursor = (val) => {
this.scrollView.scrollTo({x: val, y: 0, animated: true})
}
scrollTheView = (event) => {
this.setState({
value : Math.floor(event.nativeEvent.contentOffset.x),
cursor : Math.floor(event.nativeEvent.contentOffset.x)
})
}
checkWidth = ({nativeEvent}) => {
arrayWidth.push(nativeEvent.layout.width)
let ww = (arrayWidth[0] * this.props.data.length) - Math.round(Dimensions.get('window').width);
this.setState({
width : ww,
})
}
render() {
return (
<React.Fragment>
<ScrollView
ref={ref => this.scrollView = ref}
horizontal
style={styles.car}
scrollEventThrottle={1}
onScroll={this.scrollTheView}
showsHorizontalScrollIndicator={false}
decelerationRate={0}
//snapToInterval={200} //your element width
snapToAlignment={"center"}
>
</ScrollView>
<Slider
style={styles.containerSlide}
thumbImage={require("./../../assets/img/buttons/thumb.png")}
trackImage={require("./../../assets/img/buttons/bar.png")}
minimumValue={0}
maximumValue={this.state.width}
onValueChange={this.moveTheCursor}
value={this.state.cursor}
/>
</React.Fragment>
);
}
}
The problem is, when I use the thumb of the slider to scroll the page, it triggers the scroll that inevitably resets the position of the slider thumb, so it is not behaving correctly (flickers but it is mostly inaccurate).
Is there a way to fix this loop?
You can achieve desired behaviour using Slider's onSlidingStart and onSlidingComplete + ScrollView's scrollEnabled
It can look like
....
const [isSliding, setIsSliding] = useState(false);
....
<ScrollView scrollEnabled={!isSliding}>
<Slider
onSlidingStart={() => setIsSliding(true)}
onSlidingComplete={() => setIsSliding(false)}
/>
</ScrollView>

React native responsive header height

Im trying to set my header height so its responsive for tablets and smartphones. I've have tried using flex column:
const headerStyle = {
paddingHorizontal: theme.metrics.mainPadding,
paddingTop: 0,
paddingBottom: 0,
backgroundColor: theme.colors.blue,
flex: 0.5,
flexDirection: 'column',
};
However, the header looks ok on a tablet but too tall on a smartphone.
What is the best way to set dimensions for different devices?
EDIT:
I have this function:
export function em(value: number) {
return unit * value;
}
And I am now using it in my stylesheet:
headerHeight: em(6),
headerImageWidth: em(3),
headerImageHeight: em(3),
headerLogoHeight: em(6),
headerLogoWidth: em(20),
The image looks ok on tablet, but now on smartphone its too small. If i understand correctly, I need to use dimensions.width to set an appropriate unit value in my function?
Smartphone
Tablet
I can think of two ways
Using flex
<View style={{flex:1}}>
<View style={{flex:0.2}}>
<Text>Header</Text>
</View>
<View style={{flex:0.8}} />
</View>
You have mentioned using flex, but not working. I am not sure how exactly as if you are using it like above, size should be relative to screen size.
Using Dimensions
How about using the Dimensions module. It can be used to get the width and height of the window and you can set height based on that
import {
Dimensions
} from 'react-native';
const winWidth = Dimensions.get('window').width;
const winHeight = Dimensions.get('window').height;
header = {
height: winHeight * 0.2 // 20%
}
Then use width and height to set the height of the header (ex. percentage-based)

Categories