React Native自定义CameraRoll Hook调用报错求助及代码优化
问题分析与修正方案
你遇到的问题核心是违反了React Hook的基本规则,我来一步步拆解你的错误,再给你正确的实现方式:
错误点拆解
1. 第一次错误:在异步函数中调用Hook
React Hook明确规定:只能在函数组件的顶层作用域调用Hook,绝对不能在异步函数、循环、条件判断或者嵌套函数里调用。你最初在_pickImage这个异步函数里调用useCameraRoll,直接触发了Invalid hook call错误,这是Hook规则的红线。
2. 第二次错误:组件嵌套定义+错误的 onPress 绑定
- 你在
RenderImagePicker内部定义了MyComponent,然后把MyComponent传给onPress——onPress需要的是一个事件处理函数,而不是一个组件。这样写不仅不会触发相册加载,还会导致组件逻辑混乱。 - 你把
_handleImagePicked、_maybeRenderUploadingOverlay放在了组件外部,这些函数需要访问组件内部的状态(比如uploading),放在外部会导致作用域错误,无法正确获取状态。
正确的实现代码
我们把所有逻辑整理到同一个函数组件内部,严格遵循Hook规则:
import { useState, useCallback } from 'react'; import { TouchableOpacity, Text, View, ActivityIndicator, Image } from 'react-native'; import { useCameraRoll } from '../shared/hooks'; const RenderImagePicker = ({ setFieldValue, name, value, label, meta: { touched, error }, }) => { const [uploading, setUploading] = useState(false); // 1. 在组件顶层调用Hook,符合规则 const [photos, getPhotos] = useCameraRoll({ first: 80 }); // 2. 图片上传逻辑放在组件内部,确保能访问到组件状态 const uploadImageAsync = useCallback(async (uri) => { const apiUrl = 'http://xx:3000/upload'; const uriParts = uri.split('.'); const fileType = uriParts[uriParts.length - 1]; const formData = new FormData(); formData.append('photo', { uri, name: `photo.${fileType}`, type: `image/${fileType}` }); const options = { method: 'POST', body: formData, headers: { Accept: 'application/json', 'Content-Type': 'multipart/form-data', }, }; return fetch(apiUrl, options); }, []); const _handleImagePicked = useCallback(async (pickerResult) => { try { setUploading(true); if (!pickerResult.cancelled) { const uploadResponse = await uploadImageAsync(pickerResult.uri); const uploadResult = await uploadResponse.json(); setFieldValue(name, uploadResult.location); } } catch (e) { console.log(e); alert('Upload failed, sorry :('); } finally { setUploading(false); } }, [setFieldValue, name, uploadImageAsync]); // 3. 加载遮罩函数放在组件内部,能访问uploading状态 const _maybeRenderUploadingOverlay = useCallback(() => { if (uploading) { return ( <View> <ActivityIndicator color="#ff0000" size="large" style={{ alignSelf: 'center', flex: 1 }} /> </View> ); } }, [uploading]); return ( <> {/* 4. onPress绑定getPhotos函数,触发相册加载 */} <TouchableOpacity style={styles.button} onPress={getPhotos}> <Text style={{ color: 'white' }}>Add Photos</Text> </TouchableOpacity> {touched && error && <Text style={{ color: 'red' }}>{error}</Text>} {_maybeRenderUploadingOverlay()} {/* 可选:渲染已加载的相册图片,点击后触发上传 */} <View style={{ flexDirection: 'row', flexWrap: 'wrap', marginTop: 10 }}> {photos.map((photo, index) => ( <TouchableOpacity key={index} onPress={() => _handleImagePicked({ cancelled: false, uri: photo.uri })} style={{ margin: 5 }} > <Image source={{ uri: photo.uri }} style={{ width: 100, height: 100, borderRadius: 8 }} /> </TouchableOpacity> ))} </View> </> ); }; // 示例样式,可根据需求调整 const styles = { button: { backgroundColor: '#007AFF', padding: 12, borderRadius: 8, alignItems: 'center', marginVertical: 10, }, }; export default RenderImagePicker;
关键要点提醒
- Hook必须在组件顶层调用:所有
useState、useCallback、自定义Hook都要放在函数组件的最外层,不能嵌套在任何函数、条件里。 - 事件处理函数要正确绑定:
onPress需要接收一个函数,点击时执行这个函数的逻辑(比如调用getPhotos加载相册)。 - 状态和函数的作用域要匹配:需要访问组件状态的函数,必须定义在组件内部,或者通过依赖项传递进去(用
useCallback缓存函数,避免不必要的重渲染)。 - 自定义Hook的返回值使用:
useCameraRoll返回的photos是相册图片数组,getPhotos是加载更多图片的函数,你可以在点击按钮时调用getPhotos,然后遍历photos展示选中的图片,再绑定上传逻辑。
内容的提问来源于stack exchange,提问作者Mikk Küttim
相关产品推荐
相关产品推荐

