How to use Infinite Scroll with FlatList and React Hook? - reactjs

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

Related

Why everyone can mute anyone even after there is a moderator?

I am creating a video meet interface in react using '#jitsi/react-sdk'.When a room is created then even after there is a moderator everyone is able to mute anyone why is it happening.
Here is the code:
import { JitsiMeeting } from '#jitsi/react-sdk';
import React, { useRef, useState } from 'react';
const App = () => {
const apiRef = useRef();
const [logItems, updateLog] = useState([]);
const [users, setUsers] = useState([])
const [knockingParticipants, updateKnockingParticipants] = useState([]);
const addParticipants = payload => {
setUsers(items => [...items, payload]);
}
const removeParticipants = payload => {
setUsers(participants => participants.filter(item => item.id !== payload.id));
}
const printEventOutput = payload => {
updateLog(items => [...items, JSON.stringify(payload)]);
};
const handleAudioStatusChange = (payload, feature) => {
if (payload.muted) {
updateLog(items => [...items, `${feature} off`])
} else {
updateLog(items => [...items, `${feature} on`])
}
};
const handleChatUpdates = payload => {
if (payload.isOpen || !payload.unreadCount) {
return;
}
apiRef.current.executeCommand('toggleChat');
updateLog(items => [...items, `you have ${payload.unreadCount} unread messages`])
};
const handleKnockingParticipant = payload => {
updateLog(items => [...items, JSON.stringify(payload)]);
updateKnockingParticipants(participants => [...participants, payload?.participant])
};
const handleJitsiIFrameRef1 = iframeRef => {
iframeRef.style.border = '10px solid #3d3d3d';
iframeRef.style.background = '#3d3d3d';
iframeRef.style.height = '400px';
};
const handleApiReady = apiObj => {
apiRef.current = apiObj;
apiRef.current.on('knockingParticipant', handleKnockingParticipant);
apiRef.current.on('audioMuteStatusChanged', payload => handleAudioStatusChange(payload, 'audio'));
apiRef.current.on('videoMuteStatusChanged', payload => handleAudioStatusChange(payload, 'video'));
apiRef.current.on('raiseHandUpdated', printEventOutput);
apiRef.current.on('titleViewChanged', printEventOutput);
apiRef.current.on('chatUpdated', handleChatUpdates);
apiRef.current.on('knockingParticipant', handleKnockingParticipant);
apiRef.current.on('participantJoined', addParticipants);
apiRef.current.on('participantKickedOut', removeParticipants);
apiRef.current.on('participantLeft', removeParticipants);
};
const handleReadyToClose = () => {
/* eslint-disable-next-line no-alert */
alert('Ready to close...');
};
const generateRoomName = () => `JitsiMeetRoomNo${Math.random() * 100}-${Date.now()}`;
const renderSpinner = () => (
<div style={{
fontFamily: 'sans-serif',
textAlign: 'center'
}}>
Loading..
</div>
);
return (
<>
<h1 style={{
fontFamily: 'sans-serif',
textAlign: 'center'
}}>
JitsiMeeting App
</h1>
<JitsiMeeting
roomName={generateRoomName()}
spinner={renderSpinner}
configOverwrite={{
startWithAudioMuted: true,
startWithVideoMuted: true,
}}
config={{
hideConferenceSubject: false
}}
onApiReady={externalApi => handleApiReady(externalApi)}
onReadyToClose={handleReadyToClose}
getIFrameRef={handleJitsiIFrameRef1} />
</>
);
};
export default App;
Can anyone tell me what am I doing wrong and roundabout to get the feature implemented where only moderator can mute the mic and camera of other participants.

React infinite loop in useEffect()

I have a useEffect() which should be called once but continuously it is calling the getStories method.
const [pageNo, setPageNo] = useState(1);
const [recordsPerPage, setrecordsPerPage] = useState(5);
const { jwt } = useSelector((state: StateParams) => state.account);
useEffect(() => {
if (jwt) {
dispatch(
getStories({ token: jwt, pageNo: pageNo, recordsPerPage: recordsPerPage })
);
}
}, []);
UPDATED: I feel flatlist is causing
<FlatList
style={{
marginTop: 14,
alignSelf: "stretch"
}}
data={Items}
renderItem={renderItem}
keyExtractor={item => item.id}
/>
const renderItem = useCallback(({ item } : {id: string}) => {
console.log({item})
const Section = Components[item.id]
return (<Section {...props}/>)
}, [Items])

[Unhandled promise rejection: TypeError: undefined is not an object (evaluating '_expoMediaLibrary.MediaLibrary.createAssetAsync')]

I using expo-camera to record video and save it in my device, but even on stop recording it has video uri, the .MediaLibrary.createAssetAsync is giving the above error, also the recording gets stopped from the first even if I have used setTimeout to stop the recording, this is my code for the camera:
import React, { useState, useEffect, useRef } from 'react';
import { Text, View } from 'react-native';
import { Camera } from 'expo-camera';
import { MediaLibrary } from 'expo-media-library';
import { Audio } from 'expo-av';
export default function App() {
const [hasPermission, setHasPermission] = useState(null);
const [camera, setCamera] = useState(null);
const [recording, setRecording] = useState(false);
const [video, setVideo] = useState(null);
const [stop, setStop] = useState(false);
const recordingVideo = async () => {
const video = await camera.recordAsync();
console.log(video);
setVideo(video);
}
const saveVideo = async () => {
console.log('uri', video.uri);
const asset = await MediaLibrary.createAssetAsync(video.uri);
if (asset) {
console.log('asset', asset);
setVideo(null);
}
};
useEffect(() => {
console.log('recoring', recording);
if (recording && camera) recordingVideo();
}, [recording, camera]);
useEffect(() => {
console.log('stop', stop);
if (stop) {
setRecording(false);
camera.stopRecording();
saveVideo();
}
}, [stop]);
useEffect(() => {
(async () => {
const { status } = await Camera.requestPermissionsAsync();
const audioPerm = await Audio.requestPermissionsAsync();
setHasPermission(status === 'granted' && audioPerm.status === 'granted');
})();
}, []);
useEffect(() => {
if(camera) {
console.log('ref');
setRecording(true);
setTimeout(() => {
setStop(true);
}, 10000);
}
}, [camera]);
if (hasPermission === null) {
return <View />;
}
if (hasPermission === false) {
return <Text>No access to camera or audio</Text>;
}
return (
<View style={{ flex: 1 }}>
<Camera style={{ flex: 1 }} type={Camera.Constants.Type.front} ref={ref => setCamera(ref)}>
<View style={{ flex: 1, backgroundColor: '#00000000', justifyContent: 'center' }}>
<Text style={{ marginHorizontal: 40 }}>{recording ? 'Recording' : !stop ? 'Waiting for recording' : 'recording finished' }</Text>
</View>
</Camera>
</View>
);
}
I searched in expo-media-library docs and logged the video.uri that is exactly some matching parameter but cannot understand why it is working like that.
I was having the same problem in my Camera App. I solved by changing my import
// Change from this
import { MediaLibrary } from 'expo-media-library';
// To this
import * as MediaLibrary from 'expo-media-library';

I am trying to figure out how to create a clean up function as I keep getting an error

I am trying to figure out how to create a clean up function as I keep getting an error, if I remove "comments" from the useEffect dependencies, the error goes away, but then the app doesn't update in realtime, which is a problem. If anyone has worked with React and the realtime database or even Firestore and have any ideas on what I should do please let me know.
import React, { useContext, useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'react-toastify';
import User from '../assets/images/user.svg';
import { AuthContext } from '../helpers/firebaseAuth';
import firebase from '../helpers/Firebase';
import Loading from '../helpers/Loading';
export const Comments = ({ match, history }) => {
const { register, handleSubmit, reset } = useForm();
const slug = match.params.slug;
const {...currentUser} = useContext(AuthContext);
const [comments, setComments] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = () => {
const data = firebase.database().ref(`/posts/${slug}/comments`)
data.once('value')
.then((snapshot) => {
if (snapshot) {
let comments = [];
const snapshotVal = snapshot.val();
for (let comment in snapshotVal) {
comments.push(snapshotVal[comment]);
}
setComments(comments);
setLoading(false);
}
});
}
fetchData();
}, [slug, comments])
if (loading) {
return <Loading />;
};
const postComment = (values) => {
console.log(!!currentUser.currentUser)
if (!!currentUser.currentUser) {
const comment = {
commentText: values.commentText,
commentCreator: currentUser.currentUser.displayName,
currentUserId: currentUser.currentUser.uid,
}
const postRef = firebase.database().ref(`posts/${slug}/comments`);
postRef.push(comment);
reset();
} else {
toast.error('You are not authenticated 😕');
}
};
const deleteComment = () => {
console.log(comments[0].commentUserId);
console.log(currentUser.currentUser.uid);
if (currentUser.currentUser.uid === comments[0].commentUserId) {
console.log('correct');
}
const key = firebase.database().ref(`posts/${slug}/comments`).once('value');
key.then(snapshot => {
console.log(snapshot.val());
}).catch((error) => {
console.log(error);
});
};
const back = () => {
history.push('./');
};
return (
<div className='main' style={{ maxWidth: '600px' }}>
<div className='see-user-comments' onClick={back} style={{ cursor: 'pointer', height: '50px' }}>
Commenting on the post: {slug}
</div>
<div className='see-user-comments' style={{ padding: '10px 0' }}>
<div>
<img src={User} alt='Profile' style={{ width: '30px' }} />
<span className='usertag-span'>{currentUser.displayName}</span>
</div>
<div>
<form onSubmit={handleSubmit(postComment)}>
<textarea
name='commentText'
rows='3'
style={{ margin: '10px 0' }}
placeholder='Add to the conversation!'
ref={register}
/>
<span style={{ width: '90%' }}>
<button>Comment</button>
</span>
</form>
</div>
</div>
{comments.map((comment, index) =>
<div key={index} className='see-user-comments' style={{ padding: '15px 0' }}>
<div style={{ height: '30px' }}>
<img src={User} alt='Profile' style={{ width: '30px' }} />
<div style={{ flexDirection: 'column', alignItems: 'flex-start', justifyItems: 'center' }}>
<span className='usertag-span'>{comment.commentCreator}</span>
</div>
</div>
<span className='commentText-span'>{comment.commentText}
{ !!currentUser?.currentUser?.uid === comments[0].commentUserId ?
(<button onClick={deleteComment}>Delete</button>) : null
}
</span>
</div>
)}
</div>
)
}
export default Comments;
Without seeing the error in question, I can only assume it's because using the following pattern causes an infinite loop because the effect is re-triggered every time count changes:
const [count, setCount] = useState(0);
useEffect(() => setCount(count + 1), [count]);
When you add comments to your effect, you are doing the same thing.
To solve this, you must change your effect to rely on Firebase's realtime events to update your comments array instead. This can be as simple as changing once('value').then((snap) => {...}) to on('value', (snap) => {...});. Because this is now a realtime listener, you must also return a function that unsubscribes the listener from inside your useEffect call. The least amount of code to do this correctly is:
const [postId, setPostId] = useState('post001');
useEffect(() => {
const postRef = firebase.database().ref('posts').child(postId);
const listener = postRef.on(
'value',
postSnapshot => {
const postData = postSnapshot.val();
// ... update UI ...
},
err => {
console.log('Failed to get post', err);
// ... update UI ...
}
)
return () => postRef.off('value', listener);
}, [postId]);
Applying these changes to your code (as well as some QoL improvements) yields:
const { register, handleSubmit, reset } = useForm();
const slug = match.params.slug;
const { ...authContext } = useContext(AuthContext); // renamed: currentUser -> authContext (misleading & ambiguous)
const [comments, setComments] = useState([]);
const [loading, setLoading] = useState(true);
let _postCommentHandler, _deleteCommentHandler;
useEffect(() => {
// don't call this data - it's not the data but a reference to it - always call it `somethingRef` instead
const postCommentsRef = firebase.database().ref(`/posts/${slug}/comments`);
// create realtime listener
const listener = postCommentsRef.on(
'value',
querySnapshot => {
let _comments = [];
querySnapshot.forEach(commentSnapshot => {
const thisComment = commentSnapshot.val();
thisComment.key = commentSnapshot.key; // store the key for delete/edit operations
_comments.push(thisComment);
});
setComments(_comments);
setLoading(false);
},
err => {
console.log(`Error whilst getting comments for post #${slug}`, err);
// TODO: handle error
});
// update new comment handler
_postCommentHandler = (formData) => {
console.log({
isLoggedIn: !!authContext.currentUser
});
if (!authContext.currentUser) {
toast.error('You are not authenticated 😕');
return;
}
const newComment = {
commentText: formData.commentText, // suggested: commentText -> content
commentCreator: authContext.currentUser.displayName, // suggested: commentCreator -> author
currentUserId: authContext.currentUser.uid, // suggested: commentUserId -> authorId
}
postCommentsRef.push(newComment)
.then(() => {
// commented successfully
reset(); // reset form completely
})
.catch(err => {
console.log(`Error whilst posting new comment`, err);
// TODO: handle error
reset({ commentText: formData.commentText }) // reset form, but leave comment as-is
})
}
// update delete handler
_deleteCommentHandler = () => {
if (!comments || !comments[0]) {
console.log('Nothing to delete');
return;
}
const commentToDelete = comments[0];
console.log({
commentUserId: commentToDelete.commentUserId,
currentUser: authContext.currentUser.uid
});
if (authContext.currentUser.uid !== commentToDelete.commentUserId) {
toast.error('That\'s not your comment to delete!');
return;
}
postCommentsRef.child(commentToDelete.key)
.remove()
.then(() => {
// deleted successfully
})
.catch(err => {
console.log(`Error whilst deleting comment #${commentToDelete.key}`, err);
// TODO: handle error
});
};
// return listener cleanup function
return () => postCommentsRef.off('value', listener);
}, [slug]);
const postComment = (values) => _postCommentHandler(values);
const deleteComment = () => _deleteCommentHandler();
Because I renamed currentUser to authContext, this will also need updating:
<div>
<img src={User} alt='Profile' style={{ width: '30px' }} />
<span className='usertag-span'>{authContext?.currentUser?.displayName}</span>
</div>

React hook logging a useState element as null when it is not

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} />);
};

Resources