I want to measure the size of a React Native View every time it renders, and save it to state. If element layout didn't change the effect should not run.
It's easy to do with a class based component, where onLayout can be used. But what do I do in a functional component where I use React Hooks?
I've read about useLayoutEffect. If that's the way to go, do you have an example of how to use it?
I made this custom hook called useDimensions. This is how far I've got:
const useDimensions = () => {
const ref = useRef(null);
const [dimensions, setDimensions] = useState({});
useLayoutEffect(
() => {
setDimensions(/* Get size of element here? How? */);
},
[ref.current],
);
return [ref, dimensions];
};
And I use the hook and add the ref to the view that I want to measure the dimensions of.
const [ref, dimensions] = useDimensions();
return (
<View ref={ref}>
...
</View>
);
I've tried to debug ref.current but didn't find anything useful there. I've also tried measure() inside the effect hook:
ref.current.measure((size) => {
setDimensions(size); // size is always 0
});
If you could like a more self-contained version of this here is a custom hook version for React Native:
const useComponentSize = () => {
const [size, setSize] = useState(null);
const onLayout = useCallback(event => {
const { width, height } = event.nativeEvent.layout;
setSize({ width, height });
}, []);
return [size, onLayout];
};
const Component = () => {
const [size, onLayout] = useComponentSize();
return <View onLayout={onLayout} />;
};
You had the right idea, it just needed a couple of tweaks... mainly, handing in the element ref and using elementRef (not elementRef.current) in the useEffect dependency array.
(Regarding useEffect vs useLayoutEffect, as you're only measuring rather than mutating the DOM then I believe useEffect is the way to go, but you can swap it out like-for-like if you need to)
const useDimensions = elementRef => {
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
useEffect(() => {
const el = elementRef.current;
setDimensions({ width: el.clientWidth, height: el.clientHeight });
}, [elementRef]);
return [dimensions];
};
Use it like this:
function App() {
const divRef = useRef(null);
const [dimensions] = useDimensions(divRef);
return (
<div ref={divRef} className="App">
<div>
width: {dimensions.width}, height: {dimensions.height}
</div>
</div>
);
}
Working codesandbox here
Edited to Add React Native version:
For React Native you can use useState with onLayout like this:
const App=()=>{
const [dimensions, setDimensions] = useState({width:0, height:0})
return (
<View onLayout={(event) => {
const {x, y, width, height} = event.nativeEvent.layout;
setDimensions({width:width, height:height});
}}>
<Text}>
height: {dimensions.height} width: {dimensions.width}
</Text>
</View>
);
}
As a refinement to
matto1990's answer, and to answer Kerkness's question - here's an example custom hook that supplies the x, y position as well as the layout size:
const useComponentLayout = () => {
const [layout, setLayout] = React.useState(null);
const onLayout = React.useCallback(event => {
const layout = event.nativeEvent.layout;
setLayout(layout);
}, [])
return [layout, onLayout]
}
const Component = () => {
const [{ height, width, x, y }, onLayout] = useComponentSize();
return <View onLayout={onLayout} />;
};
Related
I am trying to use a function called readpixels from this github page and in this function one need to get the context of a canva, but since I am using React Native, I cannot use expressions like new Image() or document.createElement('canvas') so I am trying to do an equivalent using React Native functions.
Here is a minimal version of the code:
import React, { useState, useEffect, useRef } from 'react';
import { Button, Image, View } from 'react-native';
import * as ImagePicker from 'expo-image-picker';
import Canvas from 'react-native-canvas';
export function Canva() {
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
const ctx = ref.current.getContext('2d');
if (ctx) {
Alert.alert('Canvas is ready');
}
}
}, [ref]);
return (
<Canvas ref={ref} />
);
}
function readpixels(url, limit = 0) {
const img = React.createElement(
"img",
{
src: url,
},
)
const canvas = Canva()
const ctx = canvas.getContext('2d')
return 1
}
export default function ImagePickerExample() {
const [image, setImage] = useState(null);
const pickImage = async () => {
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.All,
quality: 1,
});
readpixels(result.uri)
if (!result.cancelled) {
setImage({uri: result.uri, fileSize: result.fileSize});
}
};
return (
<View style={{ flex: 1, backgroundColor: "white", marginTop: 50 }} >
<Button title="Pick image from camera roll" onPress={pickImage} />
{image && <Image source={{ uri: image.uri }} style={{ width: 200, height: 200 }} />}
</View>
);
}
And here is the error that I get:
Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
I have checked the three suggestions to solve the issue, but it did not work.
Thank you
p.s: in order to reproduce the code, you would need to install the react-native-canvas package
I want to use Pintura Editor in my react-native project, But it's not working and also didn't getting any error.
I am not able to understand where i am going wrong and does react-native supports Pintura ?
Is there any way i can solve this issue?
Any lead would be highly appreciated. Thank you in advance.
React-Native doesn't supports Pintura Editor yet. But we can use pintura with WebView, as Pintura is based on html technologies it will have to run in a WebView to work. Here is an example
Pintura_Editor.js
import { WebView } from 'react-native-webview';
import { View } from 'react-native';
import React, { forwardRef, useRef, useEffect,useState } from 'react';
const noop = () => {};
//For Making First letter uppercase.
const upperCaseFirstLetter = (str) => str.charAt(0).toUpperCase() + str.slice(1);
// PATH_TO_PINTURA.HTML file (need to download paid version)
const PinturaEditorProxy = require('./pintura.html');
const Editor = forwardRef((props, ref) => {
const { style, ...options } = props;
const webViewRef = useRef(null);
const [isStatus, setStatus] = useState(false)
setInterval(()=> {
setStatus(true)
}, 3000)
// this passes options to the editor
useEffect(() => {
clearInterval();
// setup proxy to call functions on editor
const handler = {
get: (target, prop) => {
if (prop === 'history') return new Proxy({ group: 'history' }, handler);
return (...args) => {
const name = [target.group, prop].filter(Boolean).join('.');
webViewRef.current.postMessage(
JSON.stringify({
fn: [name, ...args],
})
);
};
},
};
webViewRef.current.editor = new Proxy({}, handler);
// post new options
webViewRef.current.postMessage(JSON.stringify({ options: options }));
},[isStatus]);
return (
<View ref={ref} style={style}>
<WebView
ref={webViewRef}
javaScriptEnabled={true}
scrollEnabled={false}
domStorageEnabled={true}
allowFileAccess={true}
allowUniversalAccessFromFileURLs={true}
allowsLinkPreview={false}
automaticallyAdjustContentInsets={false}
originWhitelist={['*']}
textZoom={100}
source={PinturaEditorProxy}
onMessage={async (event) => {
// message from WebView
const { type, detail } = JSON.parse(event.nativeEvent.data);
const handler = await options[`on${upperCaseFirstLetter(type)}`] || noop;
// call handler
handler(detail);
}}
/>
</View>
);
});
export default Editor;
Now, import this editor to your app.js
import PinturaEditor from 'PATH_TO_PINTURA_EDITOR'
const editorRef = null;
....
....
render() {
return (
<PinturaEditor
ref={editorRef}
style={{ margin:40, width: '80%', height: '80%', borderWidth: 2, borderColor: '#eee' }}
imageCropAspectRatio={1}
// Image should be base64 or blob type
src={base64Image}
onLoad={(img) => {
// do something
}}
onProcess={async (image) => {
// do something with edited image
}}
/>)
}
I'm about to swap the old React Native Animated library with the new React Native Reanimated one to gain performance issues but I have encountered one problem I could not solve.
In all examples I found online, I saw that the GestureHandler, created with useAnimatedGestureHandler, is in the same component as the Animated.View. In reality that is sometimes not possible.
In my previous app, I just pass the GestureHandler object to the component via forwardRef but it seems React Native Reanimated is not able to do that. I don't know whether I have a syntax error or it is just a bug.
const App = () => {
const handlerRef = useAnimatedRef();
const y = useSharedValue(0);
handlerRef.current = useAnimatedGestureHandler({
onStart: (_, ctx) => {
ctx.startY = y.value;
},
onActive: ({translationX, translationY}, ctx) => {
y.value = translationY;
},
onEnd: () => {},
});
const animatedStyles = useAnimatedStyle(() => ({transform: [{translateY: withSpring(y.value)}]}));
const UsingHandlerDirect = () => (
<PanGestureHandler onGestureEvent={handlerRef.current} >
<Animated.View style={[styles.blueBox, animatedStyles]} />
</PanGestureHandler>
)
const UsingHandlerForwardRef = forwardRef(({animatedStyles}, ref) => (
<PanGestureHandler onGestureEvent={ref?.handlerRef?.current}>
<Animated.View style={[styles.redBox, animatedStyles]} />
</PanGestureHandler>
));
return (
<SafeAreaView>
<View style={styles.container}>
<UsingHandlerForwardRef ref={handlerRef} animatedStyles={animatedStyles}/>
<UsingHandlerDirect />
</View>
</SafeAreaView>
);
}
I have saved the GestureHandler in a useAnimatedRef handlerRef.current = useAnimatedGestureHandler({}) to make things more representable. Then I pass the the ref directly into the PanGestureHandler of the UsingHandlerDirect component. The result is that when I drag the blue box the box will follow the handler. So this version works.
But as soon as I pass the handlerRef to the UsingHandlerForwardRef component non of the gesture events get fired. I would expect that when I drag the red box will also follow the handler but it doesn't
Has someone an idea whether it's me or it's a bug in the library?
Cheers
I have given up on the idea to pass a ref around instead, I created a hook that connects both components with each other via context.
I created a simple hook
import { useSharedValue } from 'react-native-reanimated';
const useAppState = () => {
const sharedXValue = useSharedValue(0);
return {
sharedXValue,
};
};
export default useAppState;
that holds the shared value using useSharedValue from reanimated 2
The child component uses this value in the gestureHandler like that
const gestureHandler = useAnimatedGestureHandler({
onStart: (_, ctx) => {
ctx.startX = sharedXValue.value;
},
onActive: (event, ctx) => {
sharedXValue.value = ctx.startX + event.translationX;
},
onEnd: (_) => {
sharedXValue.value = withSpring(0);
},
});
and the Parent just consumes the hook value
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{
translateX: -sharedXValue.value,
},
],
};
});
I have created a workable Snack which contains the 2 components - a Child with a blue box and a Parent with a red box
So when I first load my component it fetches data from the server. The data is displayed and takes up less space than the screen. Shouldn't onEndReached be called in this situation? Or is my assumption incorrect because the data wasn't scrolled to? From my testing, onEndReached is not called on Android & IOS but I want it to.
/**
* https://reactnavigation.org/docs/4.x/typescript
*/
type Props = {
navigation: NavigationDrawerProp<{ userId: string, routeName: string }>;
}
let idCounter = 0;
export const keyExtractor = (item: any) => {
if (!item.uniqueId) {
item.uniqueId = idCounter;
idCounter++;
}
return item.uniqueId.toString();
}
const MasterScreen = (props: Props) => {
const [data, setData] = useState([]);
const loadDataFirstTime = () => {
setTimeout(() => {
let newData = [];
for (let i = 0; i < 2; i++) {
newData.push({})
}
setData(newData)
}, 4000)
}
useEffect(() => {
loadDataFirstTime();
}, []);
const renderItem = () => {
return <Text>Placeholder</Text>
}
const onEndReached = () => {
console.log("End reached with data length", data.length)
Alert.alert("Data length: " + data.length)
}
return (
<SafeAreaView style={{ flex: 1, height: height }}>
<FlatList data={data}
keyExtractor={keyExtractor}
renderItem={renderItem}
onEndReached={onEndReached}
onEndReachedThreshold={0.5}
/>
</SafeAreaView>
);
}
Using react native 0.63.4
EDIT:
Sample github repo
Relevant code inside src/screens/MasterScreen.tsx
onEndReached is not called on load but you will see the FlatList rendered
To test:
yarn
yarn android
I'd like to use React Navigation in my new react native app but I can't find any example showing how to create custom view transitions in there. Default transitions are working fine but I'd like to be able to customize them in few places and the docs don't come very helpfull in this subject.
Anyone tried that already? Anywhere I could see a working example?
Thanks in advance.
You can find detailed version of this post on this link
I hope this is clear enough with step-by-step for how to create custom transition.
Create a Scene or Two to navigate
class SceneOne extends Component {
render() {
return (
<View>
<Text>{'Scene One'}</Text>
</View>
)
}
}
class SceneTwo extends Component {
render() {
return (
<View>
<Text>{'Scene Two'}</Text>
</View>
)
}
}
Declare your app scenes
let AppScenes = {
SceneOne: {
screen: SceneOne
},
SceneTwo: {
screen: SceneTwo
},
}
Declare custom transition
let MyTransition = (index, position) => {
const inputRange = [index - 1, index, index + 1];
const opacity = position.interpolate({
inputRange,
outputRange: [.8, 1, 1],
});
const scaleY = position.interpolate({
inputRange,
outputRange: ([0.8, 1, 1]),
});
return {
opacity,
transform: [
{scaleY}
]
};
};
Declare custom transitions configurator
let TransitionConfiguration = () => {
return {
// Define scene interpolation, eq. custom transition
screenInterpolator: (sceneProps) => {
const {position, scene} = sceneProps;
const {index} = scene;
return MyTransition(index, position);
}
}
};
Create app navigator using Stack Navigator
const AppNavigator = StackNavigator(AppScenes, {
transitionConfig: TransitionConfiguration
});
Use App Navigator in your project
class App extends Component {
return (
<View>
<AppNavigator />
</View>
)
}
Register your app in eq. index.ios.js
import { AppRegistry } from 'react-native';
AppRegistry.registerComponent('MyApp', () => App);
Update #1
As for the question on how to set transition per scene, this is how I'm doing it.
When you navigate using NavigationActions from react-navigation, you can pass through some props. In my case it looks like this
this.props.navigate({
routeName: 'SceneTwo',
params: {
transition: 'myCustomTransition'
}
})
and then inside the Configurator you can switch between these transition like this
let TransitionConfiguration = () => {
return {
// Define scene interpolation, eq. custom transition
screenInterpolator: (sceneProps) => {
const {position, scene} = sceneProps;
const {index, route} = scene
const params = route.params || {}; // <- That's new
const transition = params.transition || 'default'; // <- That's new
return {
myCustomTransition: MyCustomTransition(index, position),
default: MyTransition(index, position),
}[transition];
}
}
};