Invalid hook call in a react function, ImagePicker and ActionSheet - reactjs

I really need some help with hooks in react native...
I have an ActionSheet which onPress one of the options a camera feature should open with "useCamera" function.
The "useCamera" is in another function(on another file),
using the library "ImagePicker". (specific "ImagePicker.launchCamera")
the "ImagePicker.launchCamera" set the fileUri in the callback while the function return nothing.
So to access the fileUri, I tried to use useState Hook but I get "ERROR: Invalid Hook call.Hooks can only be called inside of the body of the function..."
but the hook inside the function bodY!
ActionSheet code:
import {useCamera} from './PhotoHandle';
// export default class EditProfilePic extends React.Component {
function EditProfilePic(props) {
const [ActionSheetRef, setActionSheetRef] = useState(null);
const [fileDate, setFileDate] = useState(null);
return (
<View style={styles.images_container}>
<TouchableOpacity
style={styles.click_edit_icon}
onPress={() => ActionSheetRef.show()}>
<Image style={styles.editIcon} source={editIcon} />
</TouchableOpacity>
<Image source={props.profileSource} style={styles.image} />
<ActionSheet
ref={o => setActionSheetRef(o)}
title={
<Text style={{color: '#000', fontSize: 18}}>
Which one do you like?
</Text>
}
options={['Select from Photos', 'Camera', 'Cancel']}
cancelButtonIndex={2}
destructiveButtonIndex={2}
onPress={index => {
//camera
if (index == 1) {
useCamera()
}
}}
/>
</View>
);
}
LaunchCamera function:
export function useCamera() {
const [fileData, setFileData] = useState(null);
let options = {
storageOptions: {
skipBackup: true,
path: 'images',
},
};
ImagePicker.launchCamera(options, response => {
console.log('coolio' + response);
if (response.didCancel) {
console.log('User cancelled image picker');
} else if (response.error) {
console.log('ImagePicker Error: ', response.error);
} else if (response.customButton) {
console.log('User tapped custom button: ', response.customButton);
alert(response.customButton);
} else {
setFileData(response.data);
}
});
return fileData;
}
I tried to call useCamera() like "< useCamera / >",
I changed the anonymous function to just a function.
I tried to call useCamera() from other part of the code (not from onPress) .
I tried to use Context,and Redux.
ALL THE SAME ERROR.
please help meee,
Thanks!

I've had this problem before as well what version of React-Native are you using?
React 16.8.0 is the first release to support Hooks. When upgrading, don’t forget to update all packages, including React DOM. React Native supports Hooks since the 0.59 release of React Native.
https://reactjs.org/docs/hooks-intro.html

I see the sign, you call this.setState, it means you are using a class-based component.
a react hook can be used only in a functional component.
You have to convert the component which called LaunchCamera() (it should be renamed useCamera()) to the functional component.

Related

I'm having trouble learning how to get my React Hooks to update in real time [duplicate]

This question already has an answer here:
React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render error
(1 answer)
Closed 23 days ago.
Below is my code. How do I get my potion count to update in real time while also avoiding the "More hooks than in previous render" error.
I know there is a way to do this, but I'm struggling to understand how it works. If someone could explain it well, that would be great because I will need this to happen alot in what I'm building.
import React, { useState } from 'react';
import { View, Text, Image, StyleSheet } from 'react-native';
import CustomButton from './CustomButton';
import { useFonts } from 'expo-font';
import { NavigationContainer } from '#react-navigation/native';
import { createNativeStackNavigator } from '#react-navigation/native-stack';
import player from './Player';
import usePotion from './UsePotion';
import { useNavigation } from '#react-navigation/native';
function addPotion() {
player.potions ++;
}
function BattleScreen() {
const [fontsLoaded, error] = useFonts({
'Valorax': require('./Fonts/Valorax.otf'),
});
const navigation = useNavigation()
const [potionMessage, setPotionMessage] = useState('');
if (!fontsLoaded) {
return <Text>Loading...</Text>;
}
return (
<View style={styles.container}>
<Text style={styles.text}>{player.potions}</Text>
{potionMessage && <Text style={styles.text}>{potionMessage}</Text>}
<View style={styles.topHalf}>
</View>
<View style={styles.bottomHalf}>
<View style={styles.buttonRow}>
<CustomButton onPress={() => handleAttack()} title='Attack'></CustomButton>
<CustomButton onPress={() => handleMagic()} title='Magic'></CustomButton>
</View>
<View style={styles.buttonRow}>
<CustomButton onPress={() => usePotion(setPotionMessage)} title='Use Potion'></CustomButton>
<CustomButton onPress={() => handleRun()} title='Run'></CustomButton>
<CustomButton onPress={() => navigation.navigate('Home')} title="Home"></CustomButton>
<CustomButton onPress={() => addPotion()} title="Add Potion"></CustomButton>
</View>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#000000',
},
topHalf: {
flex: 1,
color: 'white',
},
bottomHalf: {
flex: 1,
flexDirection: 'row',
flexWrap: 'wrap'
},
buttonRow: {
flex: 1,
flexDirection: 'row',
justifyContent: 'space-evenly',
flexWrap: 'wrap'
},
text: {
fontSize: 40,
color: 'white',
fontFamily: "Valorax",
}
});
export default BattleScreen;
I think that the errors you are seeing is caused by calling the usePotion hook in your JSX. Hooks can only be called in the top level of other other hooks, or the top level of other components. I'm not sure what usePotion does with setPotionMessage, but it needs to be called at the top level of the component. If there's some event you want to be triggerable from usePotion you need to return it from usePotion:
// usePotion
export default function usePotion(onChange){
// I assume usePotion would be structured like this
const [potion,setPotion] = useState({
hp:20,
message:''
})
// im assuming usePotion have some internal data
// that you would like to setPotionMessage to
const getPotionMessage = ()=>{
// because you past setPotionMessage I assume that you
// you want to subscribe to potion message changes?
onChange?.(potion.message)
return potion.message
}
return { potion,getPotionMessage}
}
Now you have a hook that returns some state about potions, and allows you trigger other state to update:
// top level of component
const {potion,getPotionMessage} = usePotion(setPotionMessage)
.
.
.
<CustomButton onPress={getPotionMessage} title='Use Potion'/>
Finally you need to get player into react lifecycle. You could either convert Player.js into a hook, or you could you could put player into state:
// at the top level of the component
const [playerState,setPlayerState] = useState(player);
// maybe wrap addPotions in a useCallback?
const addPotion = ()=>{
setPlayerState(prev=>{
return {...prev,potions:prev.potions+1}
})
}
In order solve the potion count not updating, the short answer is that you will have to utilize a useState hook. The short reasoning for this is because react will only rerender when state is changed and it is smart enough to only update the components that have been impacted by the state change.
An example of this in your code is when the setPotionMessage method is called and the potionMessage value is updated, the only update react makes is to the JSX {potionMessage && <Text style={styles.text}>{potionMessage}</Text>}.
The long reason for why this data needs to be stored in state is the following:
When react initially renders, it compiles a DOM tree of all of your returned components. This DOM tree will look pretty similar to the JSX you have written, with additional parent tags and other stuff. When and only when there has been a change in state, React will compile a new DOM tree, called the Virtual DOM with the newly changed data. Then it will compare the existing DOM tree to the new Virtual DOM tree to find the components that have changed. Finally, react will update the original DOM tree but it will only update those components that have changed.
Continuing with the potionMessage example I used above. When BattleScreen initially renders, the Virtual DOM will render the JSX like this (pulled from the return statement):
<View>
<Text>3</Text>
<View></View>
...
</View>
However, once the setPotionMessage is called and the potionMessage value changes from '' to a message, react recompiles the Virtual DOM to determine what has changed. It will recompile to something like this:
<View>
<Text>3</Text>
<View>
<Text>You really need a potion!</Text>
</View>
...
</View>
Then it compares the original DOM tree with the Virtual DOM tree to figure out what is different. When it finds what is different, it rerenders only the part of the tree that has changed on the actual DOM tree.
<View>
<Text>3</Text>
<View>
// ONLY this block is rerendered on the DOM
<Text>You really need a potion!</Text>
// ---
</View>
...
</View>
In terms of the code modifications needed, PhantomSpooks outlined the required changes. As they mentioned, getting the player into the react lifecycle will be key.

How can I use async and await in react render

I'm developing a react-native app and I want to multi language support. I also wrote languageHelper to be easy to manage.
languageHelper.js
import AsyncStorage from '#react-native-async-storage/async-storage';
export const Translator = async (key) => {
try {
const langCode = await AsyncStorage.getItem('#lang_code')
return I18n.t(key, { locale: langCode ?? 'en' })
} catch (e) {
return I18n.t(key)
}
}
login.js
render() {
return (
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<KeyboardAvoidingView behavior={'height'} style={styles.container}>
<ScrollView>
<ImageBackground source={require('../Sources/LoginBg.jpg')} style={styles.backgroundImage}>
<Layout style={styles.header}>
<Text category='h1' status='primary'> {Translator('Login_hello')}</Text>
<Text category='h6' status='primary'> {Translator('Login_signInMessage')}</Text>
</Layout>
Error I encountered
Error: Objects are not valid as a React child (found: object with keys {_U, _V, _W, _X}). If you meant to render a collection of children, use an array instead.
But since Translator is a async func so I can't use in render. (I tried to async render).
How can I solve this? Thanks for helping.
Well this seems like a not so performant code, but if you want to make it work anyways you could return an empty string while your translation is working on it, something like this:
import AsyncStorage from '#react-native-async-storage/async-storage';
export const Translator = React.memo(({key}) => {
const [text, setText] = useState('')
React.useEffect(() => {
translate()
}, [key])
const translate = async () => {
try {
const langCode = await AsyncStorage.getItem('#lang_code')
setText(I18n.t(key, { locale: langCode ?? 'en' }))
} catch (e) {
setText(I18n.t(key))
}
}
return (
<Text>{text}</Text>
)
})
and you could use it like this in you code (using your example):
render() {
return (
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<KeyboardAvoidingView behavior={'height'} style={styles.container}>
<ScrollView>
<ImageBackground source={require('../Sources/LoginBg.jpg')} style={styles.backgroundImage}>
<Layout style={styles.header}>
<Translator('Login_hello')} />
<Translator('Login_signInMessage')} />
</Layout>
You also can make you translator accept styling props to customize the end result. Also really important to use the memo there so you don't have extra renders on your translate component as it always request from async.
Other solution
The solution I would actually recommend is to create a translation provider and configure the i18n globally there so you'll have only one storage request and your translate component will be really less expensive to load.
Success on your project.

Replacement for redux hook inside a function

I really need to have a global state inside the below function but I'm getting this error:
Invalid hook call. Hooks can only be called inside of the body of a function component
Is there a workaround or other solution for this?
import useVoiceHook from '../../hooks/voicehook';
const Sound = require('react-native-sound');
function PlaySound(name) {
const voicehook = useVoiceHook();
const voice = new Sound(name, Sound.MAIN_BUNDLE, async (error) => {
if (error) {
console.warn('failed to load the sound', error);
return;
}
if (voicehook.data.lastVoice) {
voicehook.data.lastVoice.stop();
}
voicehook.setVoice(voice);
voice.play();
});
}
export default PlaySound;
Since you try to use the PlaySound function on an event it throws you the error because you can't use a hook within it. Hooks are meant to be used as top level functions within a functional component.
Check the Rules of hooks for more details
You should pass the voicehook value as argument to playSound function and consume the same
function MyComponent(props) {
const voicehook = useVoiceHook();
...
return (
<TouchableOpacity style={{backgroundColor: "red", padding: 20}} onPress={()=> {
PlaySound('click', voicehook)
}
}>
<Text>X</Text>
</TouchableOpacity>
)
}
function PlaySound(name, voicehook) {
const voice = new Sound(name, Sound.MAIN_BUNDLE, async (error) => {
if (error) {
console.warn('failed to load the sound', error);
return;
}
if (voicehook.data.lastVoice) {
voicehook.data.lastVoice.stop();
}
voicehook.setVoice(voice);
voice.play();
});
}
export default PlaySound;

react-native-background-timer, null is not an object

I've got an error using react-native-background-timer. I would be appreciate it if you could help me solve this problem.
I'm developing a mobile app on Expo Snack, and I now want to realize the auto-delete-account function: when an account is created and not being verified for 5 minutes, it will be deleted automatically.
So, I searched about background timer and I found the library below.
https://github.com/ocetnik/react-native-background-timer
However, I wasn't able to achieve it because of the error below
(3:2693) null is not an object (evaluating 'o.setTimeout')
and this is my code
import React, { Component } from 'react';
import { View, Text, TouchableOpacity, Platform } from 'react-native';
import BackgroundTimer from 'react-native-background-timer';
let counter = 0;
let timer = null;
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
second: 0,
};
}
_interval: any;
onStart = () => {
if (Platform.OS == 'ios') {
BackgroundTimer.start();
}
this._interval = BackgroundTimer.setInterval(() => {
this.setState({
second: this.state.second + 1,
});
}, 1000);
};
onPause = () => {
BackgroundTimer.clearInterval(this._interval);
};
onReset = () => {
this.setState({
second: 0,
});
BackgroundTimer.clearInterval(this._interval);
};
render() {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<TouchableOpacity onPress={this.onStart}>
<Text>start</Text>
</TouchableOpacity>
<TouchableOpacity onPress={this.onPause}>
<Text>pause</Text>
</TouchableOpacity>
<TouchableOpacity onPress={this.onReset}>
<Text>reset</Text>
</TouchableOpacity>
<Text>{this.state.second}</Text>
</View>
);
}
}
I followed a tutorial of this guy
https://medium.com/#loons.create/how-to-setup-react-native-background-timer-22263d655847
The equipped function, setInterval of javascript and etc. of course works fine as a timer, but actually they don't work behind in react native.
What am I missing, or is this an issue inside this library( I suppose so )? If so, please tell me an available version of this library; I use the latest version, 2.2.0, and React v35.0.0
Thank you
You cannot use "react-native-background-timer" with Expo on managed workflow. This library needs to compile some native code.
Instead, you should take a took to Expo BackgroundFetch which is doing almost the same thing.
https://docs.expo.io/versions/latest/sdk/background-fetch/
Using Expo components, you don't need to eject or compile additional native code.

How to properly update/re-render a component that is not a child React Native?

I'm using a react-navigation. More specifically, I have a materialTabNavigator nested inside of a drawerNavigator. Each tab is in itself a stackNavigator. I have a button in homeScreen, that navigates to makePost.js. There I take in information and store it to Async storage using a simple wrapper.
In Posts.js there's a FlatList displaying each post as a component. The data for the FlatList is initially set correctly after making a request from Async Storage. The problem is that this only happens when the app is first opened. I have tried many different approaches to solve this. The only way so far I've found is to continuously setState in ComponentDidUpdate() in Posts.js. Obviously this is problematic, because it re-renders constantly. I can set a flag to stop is from rendering, but then it will not re-render again.
Ultimately, what I'd like to happen is that when I hit the user is done entering their information and is ready to make a post, they hit the button in makePost.js, and the data in the FlatList of Posts.js is update.
I've tried to pass parameters using navigation, does not work, parameters get lost somewhere, probably because of the nested navigators.
I could really used some guidance on the proper way to accomplish this.
( Navigators; not sure why this is forcing to one line )
---drawer
--tabNav
-home
homeScreen.js
makePost.js
-posts
posts.js
-messages
--drawer1
--drawer2
//Posts.js
export default class Posts extends React.Component {
state = {
rows: [
{id: 0, text: "dog"},
],
}
componentDidMount() {
this.loadState();
}
loadState = () => {
var value = store.get('posts').then((res => {
if (res === null) {
res = [{id: 0, text: "default"}]
} else {
res = res
}
this.setState({rows: res})
}))
}
componentDidUpdate() {
this.loadState();
}
renderItem = ({item}) => {
return (
<BoardTab style={styles.row} />
)}
render() {
return (
<View style={styles.view}>
<FlatList
ListFooterComponent={this.renderFooter}
style={styles.container}
data={this.state.rows}
renderItem={this.renderItem}
keyExtractor={extractKey}
>
</FlatList>
<BoardScreenFooter />
</View>
);
}
And Posts.js button looks like this:
<TouchableOpacity
onPress={ () => {
this._onPressButton
this.storeFunc(this.state.newPost)
const retval = this.state.rows
this.props.navigation.navigate('Board',
{rowsID: retval});
}
}>
<Icon
reverse
name='md-camera'
type='ionicon'
color='green'
size={12}
/>
</TouchableOpacity>
storeFunc(newObj) {
newObj.id = newObj.id + 1
store.push('posts', newObj)
store.get('posts').then((res) => {
this.setState({rows: res})
})
}
Rapidly, i would say: use Redux. It alloq you to have global state in your app, which mean you can access the state anywhere (And also set them anywhere)
When opening the app, you get the data from the AsyncStore into the Redux store. You listen to the redux state (Which will be a props in your component) and display your list. When modifying your list in the other tab, you need to do 2 things:
Store the new data in the AsyncStorage
Update the state in the redux store. Since Posts.js will be listening at the redux store (as a props), it will re-render each time your data will change
A simple way to re-render a React-Navigation screen view on navigating to it:
All credit goes to Andrei Pfeiffer, Jul 2018, in his article: "Handle Tab changes in React Navigation v2" https://itnext.io/handle-tab-changes-in-react-navigation-v2-faeadc2f2ffe
I will reiterate it here in case the above link goes dead.
Simply add a NavigationEvents component to your render function with the desired listener prop:
render() {
return (
<View style={styles.view}>
<NavigationEvents
onWillFocus={payload => {
console.log("will focus", payload);
this.loadState();
}}
/>
<FlatList
ListFooterComponent={this.renderFooter}
style={styles.container}
data={this.state.rows}
renderItem={this.renderItem}
keyExtractor={extractKey}
>
</FlatList>
<PostScreenFooter />
</View>
);
}

Resources