I have a form object that holds all my input values. I am working with the react-native camera component and what I am trying to accomplish is every time a picture is added, it is added to the object. With my current code, I am getting invalid attempts to spread non-iterable instances.
import { TicketContext } from "../store/TicketContext";
function Pictures() {
const [hasCameraPermissions, setHasCameraPermissions] = useState();
const [picture, setPicture] = useState();
const { form, setForm } = useContext(TicketContext);
//Add picture to form //
const handleAddPicture = () => {
setForm([...(form ?? []), { picture: picture.uri }]);
setPicture();
};
const cameraRef = useRef();
//Get Permission to use Camera//
const handleCameraPermissions = async () => {
const cameraPermissions = await Camera.requestCameraPermissionsAsync();
setHasCameraPermissions(cameraPermissions.status === "granted");
};
// Check for permissions on load //
useEffect(() => {
handleCameraPermissions();
}, []);
if (hasCameraPermissions === undefined) {
return <Text>Permissions Required...</Text>;
} else if (!hasCameraPermissions) {
return <Text>Camera Permission Denied. Please change in settings.</Text>;
}
//Take Picture //
const handleTakePicture = async () => {
const options = { base64: true, exif: false, quality: 1 };
const newPicture = await cameraRef.current.takePictureAsync(options);
setPicture(newPicture);
};
if (picture) {
return (
<SafeAreaView style={styles.container}>
<Image
style={styles.preview}
source={{ uri: "data:image/jpg;base64," + picture.base64 }}
/>
<View style={styles.buttonContainer}>
<Button title="Add Picture" onPress={handleAddPicture} />
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={styles.container}>
<Camera style={styles.cameraContainer} ref={cameraRef}>
<View style={styles.buttonContainer}>
<Button title="Take Picture" onPress={handleTakePicture} />
</View>
</Camera>
</SafeAreaView>
);
}
export default Pictures;
setPicture returns the state back to undefined
my state in context
const [form, setForm] = useState({});
//[form ,setForm]
// make sure you object keys are camel case.
const handleAddPicture = () => {
setForm([...(form ?? []), { picture: picture.uri }]);
setPicture();
};
Related
I am trying to save a value to async storage and then navigate to the right page depending on what the value outcome is from the Async storage. I can store data in AsyncStorage but my states does not update, I have to reload the app in order for the state to update. here is my code:
Here I have a Welcome/Obnoarding screen. I want this screen to only show to the new app users. So when a user presses the continue button I want to save a value to the Async storage so that the next time they log in they don't have to see the onboarding page again. Here is my Onboarding page:
const WelcomeScreen: FC<IWelcomeScreen> = ({ navigation }) => {
const { width, height } = Dimensions.get("window");
const btnText = "Contiunue";
const title = "Book";
const subTitle = "Fab";
let [fontsLoaded] = useFonts({
PinyonScript_400Regular,
});
const continueBtn = async () => {
try {
await AsyncStorage.setItem('#viewedOnboarding', 'true');
} catch (error) {
console.log('Error #setItem: ', error);
};
};
if (!fontsLoaded) {
return <Text>...Loading</Text>;
} else {
return (
<View style={containerStyle(height, width).container}>
<ImageBackground
resizeMode={"cover"}
style={styles.image}
source={require("../assets/model.jpg")}
>
<LinearGradient
colors={["#00000000", "#000000"]}
style={styles.gradient}
>
<View style={styles.container}>
<View style={styles.logoTextContainer}>
<Text style={styles.logoText}>{title}</Text>
<Text style={styles.logoText}>{subTitle}</Text>
</View>
<ContinueBtn label={btnText} callback={continueBtn} />
</View>
</LinearGradient>
</ImageBackground>
</View>
);
}
};
In my AppNavigator I want to decide which navigation the user should see. But when I press the continue page my app does not navigate to my TabsNavigator. It stays on my Onboarding page but if I refresh the app then the app navigates to my Tabs navigator. here is the code where I determine where the user should be depending if they are a new user or a "old" user:
const WelcomeScreen: FC<IWelcomeScreen> = ({ navigation }) => {
const { width, height } = Dimensions.get("window");
const btnText = "Contiunue";
const title = "Book";
const subTitle = "Fab";
let [fontsLoaded] = useFonts({
PinyonScript_400Regular,
});
const continueBtn = async () => {
try {
await AsyncStorage.setItem('#viewedOnboarding', 'true');
} catch (error) {
console.log('Error #setItem: ', error);
};
};
if (!fontsLoaded) {
return <Text>...Loading</Text>;
} else {
return (
<View style={containerStyle(height, width).container}>
<ImageBackground
resizeMode={"cover"}
style={styles.image}
source={require("../assets/model.jpg")}
>
<LinearGradient
colors={["#00000000", "#000000"]}
style={styles.gradient}
>
<View style={styles.container}>
<View style={styles.logoTextContainer}>
<Text style={styles.logoText}>{title}</Text>
<Text style={styles.logoText}>{subTitle}</Text>
</View>
<ContinueBtn label={btnText} callback={continueBtn} />
</View>
</LinearGradient>
</ImageBackground>
</View>
);
}
};
Setting a value in the async storage will not trigger a rerender of your AppNavigator. Thus, if the user presses the continue button, then nothing will happen visually, since the state of AppNavigator has not changed. If you refresh the app, the flag, which you have set previously using the setItem function, will be reloaded in AppNavigator on initial rendering. This is the reason why it works after refreshing the application.
For this kind of problem, I would suggest that you use a Context for triggering a state change in AppNavigator.
Here is a minimal example on how this would work. I have added comments in the code to guide you.
For the sake of simplicity, we will make the following assumption:
We have two screens in a Stack, one is the WelcomeScreen, the other one is called HomeScreen.
Notice that we use conditional rendering for the screens depending on our application context. You can add whatever screens you want, even whole navigators (this would be necessary if your navigators are nested, but the pattern stays the same).
App
export const AppContext = React.createContext()
const App = () => {
// it is important that the initial state is undefined, since
// we need to wait for the async storage to return its value
// before rendering anything
const [hasViewedOnboarding, setHasViewedOnboarding] = React.useState()
const appContextValue = useMemo(
() => ({
hasViewedOnboarding,
setHasViewedOnboarding,
}),
[hasViewedOnboarding]
)
// retrieve the onboarding flag from the async storage in a useEffect
React.useEffect(() => {
const init = async () => {
const value = await AsyncStorage.getItem('#viewedOnboarding')
setHasViewedOnboarding(value != null ? JSON.parse(value) : false)
}
init()
}, [])
// as long as the flag has not been loaded, return null
if (hasViewedOnboarding === undefined) {
return null
}
// wrap everything in AppContext.Provider an pass the context as a value
return (
<AppContext.Provider value={appContextValue}>
<NavigationContainer>
<Stack.Navigator>
{!hasViewedOnboarding ? (
<Stack.Screen name="Welcome" component={WelcomeScreen} />
) : (
<Stack.Screen
name="Home"
component={HomeScreen}
/>
)}}
</Stack.Navigator>
</NavigationContainer>
</AppContext.Provider>
)
}
Now, in your WelcomeScreen you need to access the context and set the state after the async value has been stored.
const WelcomeScreen: FC<IWelcomeScreen> = ({ navigation }) => {
// access the context
const { setHasViewedOnboarding } = useContext(AppContext)
const { width, height } = Dimensions.get("window");
const btnText = "Contiunue";
const title = "Book";
const subTitle = "Fab";
let [fontsLoaded] = useFonts({
PinyonScript_400Regular,
});
const continueBtn = async () => {
try {
await AsyncStorage.setItem('#viewedOnboarding', 'true');
setHasViewedOnboarding(true)
} catch (error) {
console.log('Error #setItem: ', error);
};
};
if (!fontsLoaded) {
return <Text>...Loading</Text>;
} else {
return (
<View style={containerStyle(height, width).container}>
<ImageBackground
resizeMode={"cover"}
style={styles.image}
source={require("../assets/model.jpg")}
>
<LinearGradient
colors={["#00000000", "#000000"]}
style={styles.gradient}
>
<View style={styles.container}>
<View style={styles.logoTextContainer}>
<Text style={styles.logoText}>{title}</Text>
<Text style={styles.logoText}>{subTitle}</Text>
</View>
<ContinueBtn label={btnText} callback={continueBtn} />
</View>
</LinearGradient>
</ImageBackground>
</View>
);
}
};
I am trying to render the first and last name from a json request using axios.
I am getting the following error you see in the title. I have included a snack example here reproducing the error exactly as well as added the code below.
Thank you
const plsWork = () => {
// Make a request for a user with a given ID
return axios.get('https://randomuser.me/api')
.then(({data}) => {
console.log(data);
return data
})
.catch(err => {
console.error(err);
});
}
const userName = (userInfo) => {
const {name: {first, last}} = userInfo;
return {first}, {last};
}
export default function App() {
const [data, setData] = React.useState(' ')
const [userInfos, setUserInfos] = React.useState([]);
React.useEffect(() => {
plsWork().then(randomData => {
setData(JSON.stringify(randomData, null, 4) || 'No user data found.')
setUserInfos(randomData.results)
})
}, []);
return (
<View>
<ScrollView>
{
userInfos.map((userInfo, idx) => (
<Text key={idx}>
{userName(userInfo)}
</Text>
))
}
<Text style={{color: 'black', fontSize: 15}}>
{data}
</Text>
</ScrollView>
</View>
);
}
You have to return a React Component in the userName function.
In the line 21:
Change from return {first}, {last} to return <>{first}, {last}</>.
It should work!
Here is code edited: snack expo
I have a Delete on Swipe function that returns TypeError: undefined is not an object(evaluating 'singleValue.StopTracking')
Im using expo to run the project
After I swipe, expo returns the error, I click dismiss and the product disappears from my cart
I don't know what any more information to give
const rowTranslateAnimatedValues = {};
Array(20)
.fill('')
.forEach((_, i) => {
rowTranslateAnimatedValues[${i}] = new Animated.Value(1);
});
const [cart, setCart] = useState([]);
const [products, setProducts] = useState([]);
const [valueInput, setValueInput] = useState(1);
const [listData, setListData] = useState(
Array(4)
.fill('')
.map((_, i) => ({ key: `${i}`, text: `item #${i}` }))
);
const onSwipeValueChange = swipeData => {
const { key, value } = swipeData;
if (
value < -Dimensions.get('window').width &&
!this.animationIsRunning
) {
this.animationIsRunning = true;
Animated.timing(rowTranslateAnimatedValues[key], {
toValue: 0,
duration: 500,
})
.start(() => {
const newData = [...listData];
const prevIndex = listData.findIndex(item => item.key === key);
newData.splice(prevIndex, 1);
setListData(newData);
this.animationIsRunning = false;
});
}
};
const renderItem = data => (
<Animated.View
>
<TouchableHighlight
onPress={() => console.log('You touched me')}
style={styles.rowFront}
underlayColor={'#AAA'}
>
<View>
<Text>I am {data.item.text} in a SwipeListView</Text>
</View>
</TouchableHighlight>
</Animated.View>
);
const renderHiddenItem = () => (
<View style={styles.rowBack}>
<View style={[styles.backRightBtn, styles.backRightBtnRight]}>
<Text style={styles.backTextWhite}>Delete</Text>
</View>
</View>
);
return (
<Block flex center style={styles.cart}>
<View style={styles.container}>
<SwipeListView
disableRightSwipe
data={cart}
renderItem={renderProduct}
showsVerticalScrollIndicator={false}
keyExtractor={(item, index) => `${index}-${item.title}`}
ListEmptyComponent={renderEmpty()}
ListHeaderComponent={renderHeader()}
ListFooterComponent={renderFooter()}
renderHiddenItem={renderHiddenItem}
rightOpenValue={-Dimensions.get('window').width}
onSwipeValueChange={onSwipeValueChange}
/>
</View>
</Block>
);
}
For anyone who faces this problem,
you can just think this.animationIsRunning of a variable that manages the start and the end of animation.
So you can just use const animationIsRunning = useRef(false) to manage the animation state.
You can use animationIsRunning.current instead of this.animationIsRunning.
Also, this.animationIsRunning = true or false can be changed into animationIsRunning.current = true or false
See code at
https://github.com/jemise111/react-native-swipe-list-view/pull/521/commits/2946e3a123f26ef5292a3884e0aec58436386e18
I'm refactoring to React Hooks but I can't get Infinite Scroll with FlatList working.
const [page, setPage] = useState(1);
This is my useEffect Hook:
useEffect(() => {
const loadProducts = async () => {
setIsLoading(true);
let response = await fetch(`${api}&page=${page}&perPage=5`);
let results = await response.json();
setProducts([...products, ...results.data]);
setIsLoading(false);
};
loadProducts();
}, [page]);
Offset is ${page}, limit is &perPage=5 (hardcoded to 5)
Flatlist:
<FlatList
data={products}
keyExtractor={(item) => item.id}
renderItem={renderGridItem}
onEndReached={loadMore}
onEndThreshold={0.3}
/>;
LoadMore:
const loadMore = () => {
setPage(page + 1);
};
In theory, this should work, shouldn't it?
Description
I was struggling a lot with this myself. Here's an example using a SectionList (basically the same as a Flatlist)
The header numbers indicates the request number send to the API. You can check that the request are in the correct order and that there are no duplicates, by clicking the "Check Numbers" button.
In this example we use reqres.in to simulate a fetch to some data.
The example also implements pull-to-refresh. Again, you can check that the length of the array is as expected after a pull-to-refresh by clicking the "Check length" button.
Expo snack
A snack of the example can be found here: https://snack.expo.io/BydyF9yRH
Make sure to change platform to iOS or Android in the snack (Web will not work)
Code
import * as React from 'react';
import { ActivityIndicator } from 'react-native'
var _ = require('lodash')
import {
StyleSheet,
Text,
View,
SafeAreaView,
SectionList,
Button,
RefreshControl
} from 'react-native';
function Item(item) {
return (
<View style={styles.item}>
<Text style={styles.title}>{item.title.first_name}</Text>
</View>
);
}
export default function testSectionList({ navigation }) {
const [data, setData] = React.useState()
const [loading, setLoading] = React.useState(true)
const [refreshing, setRefreshing] = React.useState(false);
const [showRefreshingIndicator, setShowRefreshingIndicator] = React.useState(false);
const dataIndex = React.useRef(0);
const totalHits = React.useRef(42); // In real example: Update this with first result from api
const fetchData = async (reset: boolean) => {
if (reset === true) dataIndex.current = 0;
// Make sure to return if no more data from API
if (dataIndex.current !== 0 && dataIndex.current >= totalHits.current) return []
// For example usage, select a random page
const fakepage = Math.round(Math.random()) * 2
const resultObject = await fetch(`https://reqres.in/api/users?page=${fakepage}`);
const result = await resultObject.json()
dataIndex.current++;
return {
title: `${dataIndex.current-1}`,
data: await result.data
}
}
const count = () => {
alert(data.length)
}
const checkPageNumbers = () => {
const numbers = data.map(item => parseInt(item.title))
const incremental = [...Array(data.length).keys()]
alert(_.isEqual(numbers, incremental))
}
const getInitialData = async () => {
const list = await fetchData(false)
if(!list) return
setData([list])
setLoading(false)
}
React.useEffect(() => {
getInitialData()
}, [])
const onEndReached = async () => {
const newItems = await fetchData(false)
if(!newItems.data.length) return
setData([...data, newItems])
}
const onRefresh = React.useCallback(async () => {
setShowRefreshingIndicator(true);
const newItems = await fetchData(true)
setData([newItems])
setShowRefreshingIndicator(false)
}, [refreshing]);
if (loading) return <Text>Loading...</Text>
return (
<SafeAreaView style={styles.container}>
<Button title={"Check numbers"} onPress={() => checkPageNumbers()} />
<Button title={"Check length"} onPress={() => count()} />
<SectionList
sections={data}
refreshing={refreshing}
refreshControl={
<RefreshControl refreshing={showRefreshingIndicator} onRefresh={onRefresh} />
}
onEndReached={() => {
if(refreshing) return;
setRefreshing(true)
onEndReached().then(() => {
setRefreshing(false)
})
}}
onEndReachedThreshold={1}
keyExtractor={(item, index) => item + index}
renderItem={({ item }) => <Item title={item} />}
renderSectionHeader={({ section: { title } }) => (
<Text style={styles.header}>{title}</Text>
)}
ListFooterComponent={<ActivityIndicator size={"large"} />}
stickySectionHeadersEnabled={false}
/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: 40,
marginHorizontal: 16,
},
item: {
backgroundColor: '#f9c2ff',
padding: 2,
marginVertical: 2,
},
header: {
fontSize: 16,
},
title: {
fontSize: 12,
},
});
Try to use useCallback instead of useEffect on this case. Also, I've shown you how you can prevent spreading null result to setState.
const loadProducts = async () => {
setIsLoading(true);
let response = await fetch(`${api}&page=${page}&perPage=5`);
let results = await response.json();
if (result.data) {
setProducts([...products, ...results.data]);
}
setIsLoading(false);
};
useEffect(() => {
loadProducts();
}, [])
const onLoadMore = useCallback(() => {
loadProducts();
}
for more information about useCallback, please read this. https://reactjs.org/docs/hooks-reference.html#usecallback
I have a method,
const handleUpvote = (post, index) => {
let newPosts = JSON.parse(JSON.stringify(mappedPosts));
console.log('mappedPosts', mappedPosts); // null
console.log('newPosts', newPosts); // null
if (post.userAction === "like") {
newPosts.userAction = null;
} else {
newPosts.userAction = "like";
}
setMappedPosts(newPosts);
upvote(user.id, post._id);
};
That is attached to a mapped element,
const mapped = userPosts.map((post, index) => (
<ListItem
rightIcon = {
onPress = {
() => handleUpvote(post, index)
}
......
And I have
const [mappedPosts, setMappedPosts] = useState(null);
When the component mounts, it takes userPosts from the redux state, maps them out to a ListItem and appropriately displays it. The problem is that whenever handleUpvote() is entered, it sees mappedPosts as null and therefore sets the whole List to null at setMappedPosts(newPosts);
What am I doing wrong here? mappedPosts is indeed not null at the point when handleUpvote() is clicked because.. well how can it be, if a mappedPosts element was what invoked the handleUpvote() method in the first place?
I tried something like
setMappedPosts({
...mappedPosts,
mappedPosts[index]: post
});
But that doesn't even compile. Not sure where to go from here
Edit
Whole component:
const Profile = ({
navigation,
posts: { userPosts, loading },
auth: { user, isAuthenticated },
fetchMedia,
checkAuth,
upvote,
downvote
}) => {
const { navigate, replace, popToTop } = navigation;
const [mappedPosts, setMappedPosts] = useState(null);
useEffect(() => {
if (userPosts) {
userPosts.forEach((post, index) => {
post.userAction = null;
post.likes.forEach(like => {
if (like._id.toString() === user.id) {
post.userAction = "liked";
}
});
post.dislikes.forEach(dislike => {
if (dislike._id.toString() === user.id) {
post.userAction = "disliked";
}
});
});
const mapped = userPosts.map((post, index) => (
<ListItem
Component={TouchableScale}
friction={100}
tension={100}
activeScale={0.95}
key={index}
title={post.title}
bottomDivider={true}
rightIcon={
<View>
<View style={{ flexDirection: "row", justifyContent: "center" }}>
<Icon
name="md-arrow-up"
type="ionicon"
color={post.userAction === "liked" ? "#a45151" : "#517fa4"}
onPress={() => handleUpvote(post, index)}
/>
<View style={{ marginLeft: 10, marginRight: 10 }}>
<Text>{post.likes.length - post.dislikes.length}</Text>
</View>
<Icon
name="md-arrow-down"
type="ionicon"
color={post.userAction === "disliked" ? "#8751a4" : "#517fa4"}
onPress={() => handleDownvote(post, index)}
/>
</View>
<View style={{ flexDirection: "row" }}>
<Text>{post.comments.length} comments</Text>
</View>
</View>
}
leftIcon={
<View style={{ height: 50, width: 50 }}>
<ImagePlaceholder
src={post.image.location}
placeholder={post.image.location}
duration={1000}
showActivityIndicator={true}
activityIndicatorProps={{
size: "large",
color: index % 2 === 0 ? "blue" : "red"
}}
/>
</View>
}
></ListItem>
));
setMappedPosts(mapped);
} else {
checkAuth();
fetchMedia();
}
}, [userPosts, mappedPosts]);
const handleDownvote = (post, index) => {
let newPosts = JSON.parse(JSON.stringify(mappedPosts));
if (post.userAction === "dislike") {
newPosts.userAction = null;
} else {
newPosts.userAction = "dislike";
}
setMappedPosts(newPosts);
downvote(user.id, post._id);
};
const handleUpvote = post => {
let newPosts = JSON.parse(JSON.stringify(mappedPosts));
console.log("mappedPosts", mappedPosts); // null
console.log("newPosts", newPosts); // null
if (post.userAction === "like") {
newPosts.userAction = null;
} else {
newPosts.userAction = "like";
}
setMappedPosts(newPosts);
upvote(user.id, post._id);
};
return mappedPosts === null ? (
<Spinner />
) : (
<ScrollView
refreshControl={
<RefreshControl
refreshing={false}
onRefresh={() => {
this.refreshing = true;
fetchMedia();
this.refreshing = false;
}}
/>
}
>
{mappedPosts}
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center"
}
});
Profile.propTypes = {
auth: PropTypes.object.isRequired,
posts: PropTypes.object.isRequired,
fetchMedia: PropTypes.func.isRequired,
checkAuth: PropTypes.func.isRequired,
upvote: PropTypes.func.isRequired,
downvote: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
auth: state.auth,
posts: state.posts
});
export default connect(
mapStateToProps,
{ fetchMedia, checkAuth, upvote, downvote }
)(Profile);
The reason why your current solution doesn't work is because you're rendering userPosts inside of the useEffect hook, which looks like it only runs once, ends up "caching" the initial state, and that's what you end up seeing in your handlers.
You will need to use multiple hooks to get this working properly:
const Profile = (props) => {
// ...
const [mappedPosts, setMappedPosts] = useState(null)
const [renderedPosts, setRenderedPosts] = useState(null)
useEffect(() => {
if (props.userPosts) {
const userPosts = props.userPosts.map(post => {
post.userAction = null;
// ...
})
setMappedPosts(userPosts)
} else {
checkAuth()
fetchMedia()
}
}, [props.userPosts])
const handleDownvote = (post, index) => {
// ...
setMappedPosts(newPosts)
}
const handleUpvote = (post) => {
// ...
setMappedPosts(newPosts)
}
useEffect(() => {
if (!mappedPosts) {
return
}
const renderedPosts = mappedPosts.map((post, index) => {
return (...)
})
setRenderedPosts(renderedPosts)
}, [mappedPosts])
return !renderedPosts ? null : (...)
}
Here's a simplified example that does what you're trying to do:
CodeSandbox
Also, one note, don't do this:
const Profile = (props) => {
const [mappedPosts, setMappedPosts] = useState(null)
useEffect(() => {
if (userPosts) {
setMappedPosts() // DON'T DO THIS!
} else {
// ...
}
}, [userPosts, mappedPosts])
}
Stay away from updating a piece of state inside of a hook that has it in its dependency array. You will run into an infinite loop which will cause your component to keep re-rendering until it crashes.
Let me use a simplified example to explain the problem:
const Example = props => {
const { components_raw } = props;
const [components, setComponents] = useState([]);
const logComponents = () => console.log(components);
useEffect(() => {
// At this point logComponents is equivalent to
// logComponents = () => console.log([])
const components_new = components_raw.map(_ => (
<div onClick={logComponents} />
));
setComponents(components_new);
}, [components_raw]);
return components;
};
As you can see the cycle in which setComponents is called, components is empty []. Once the state is assigned, it stays with the value logComponents had, it doesn't matter if it changes in a future cycle.
To solve it you could modify the necessary element from the received data, no components. Then add the onClick on the return in render.
const Example = props => {
const { data_raw } = props;
const [data, setData] = useState([]);
const logData = () => console.log(data);
useEffect(() => {
const data_new = data_raw.map(data_el => ({
...data_el // Any transformation you need to do to the raw data.
}));
setData(data_new);
}, [data_raw]);
return data.map(data_el => <div {...data_el} onClick={logData} />);
};