Sending a react-redux action to an API to return posts when the user activates drag refresh or accesses the component for the first time.
This normally does not cause a problem as the FlatList would take directly from props and therefore not trigger a recursive loop. However, I need to format the data to fit the application and as a result, I pass the data through a series of methods before it hits the FlatList. This process is clashing with componentDidUpdate thus throwing the application into a loop of requests. Code below:
componentDidUpdate(prevProps) {
if (prevProps.posts !== this.props.posts){
this.storePosts() //the problem is that when storeposts is called it is updating the props which then is calling store posts which is updating the props and so on and so on....
}
}
storePosts() { //the problem with this is that it changes the state
this.props.fetchPosts() //this line is causing a continous request to rails
let itemData = this.props.posts
if (itemData !== this.state.currentlyDisplayed) {
this.setState({ currentlyDisplayed: this.props.posts, items: itemData }, () => {
this.setState({isLoading: false})
});
}
}
formatItems = () => {
const itemData = this.props.posts
const newItems = [itemData]
const numberOfFullRows = Math.floor(itemData.length / 3);
let numberOfElementsLastRow = itemData.length - (numberOfFullRows * 3);
while (numberOfElementsLastRow !== 3 && numberOfElementsLastRow !== 0) {
newItems.push({ key: `blank-${numberOfElementsLastRow}`, empty: true });
numberOfElementsLastRow++;
}
return this.setState({ newItems: newItems})
// console.log(newItems.length, numberOfElementsLastRow)
};
renderItem = ({ item, type }) => {
const { items } = this.state;
if (item.empty === true) {
return <View style={[styles.item, styles.itemInvisible]} />;
} else {
return (
<TouchableOpacity style={styles.item} onPressIn={() => this.setState({ itemSelected: item.id })} onPress={() => this.props.navigation.navigate('View Post', {post: item})} key={item.id}>
<Image source={{ uri: item.image }} style={{ flex: 1, width: '100%', height: undefined }} />
</TouchableOpacity>
);
}
};
onRefresh = () => {
this.setState({refreshing: true})
this.storePosts()
this.setState({refreshing: false, currentlyDisplayed: this.props.posts})
};
render() {
const { error: { vaultErrorMessage }} = this.props
const { posts } = this.props
<SafeAreaView>
<FlatList
data={this.state.currentlyDisplayed}
renderItem={this.renderItem}
numColumns={3}
keyExtractor={(item, index) => index.toString()}
refreshControl={<RefreshControl refreshing={this.state.refreshing} onRefresh={() => this.onRefresh()} />}
extraData={this.state.refresh}
/>
</SafeAreaView>
);
}
}
}
If anyone has any ideas to go about this better or solve the problem that would be great! I think I have been looking at the code for too long so I'm pretty dulled to thinking about how to solve....
I suggest splitting the logic for updating the posts and storing the posts into two separate methods to avoid the infinite state update.
For example:
componentDidUpdate(prevProps) {
if (shouldUpdate) { // some other condition
this.updatePosts();
}
if (prevProps.posts !== this.props.posts){
this.storePosts();
}
}
updatePosts() {
this.props.fetchPosts();
}
storePosts() {
let itemData = this.props.posts;
if (itemData !== this.state.currentlyDisplayed) {
this.setState({ currentlyDisplayed: this.props.posts, items: itemData }, () => {
this.setState({isLoading: false})
});
}
}
You should also look into a better way to check if the posts have actually changed since the array may have changed but the content may have stayed the same. (Note that [1, 2, 3] === [1, 2, 3] evaluates to false). fast-deep-equal is a good library for this, or you can come up with a custom solution.
If you have to use this approach, use static getDerivedStateFromProps instead of componentDidUpdate.
getDerivedStateFromProps is invoked right before calling the render method, both on the initial mount and on subsequent updates. It should return an object to update the state, or null to update nothing.
This method exists for rare use cases where the state depends on changes in props over time. For example, it might be handy for implementing a <Transition> component that compares its previous and next children to decide which of them to animate in and out.
You can try it;
extraData={this.state.currentlyDisplayed}
Related
The Goal:
My React Native App shows a list of <Button /> based on the value from a list of Object someData. Once a user press a <Button />, the App should shows the the text that is associated with this <Button />. I am trying to achieve this using conditional rendering.
The Action:
So first, I use useEffect to load a list of Boolean to showItems. showItems and someData will have the same index so I can easily indicate whether a particular text associated with <Button /> should be displayed on the App using the index.
The Error:
The conditional rendering does not reflect the latest state of showItems.
The Code:
Here is my code example
import {someData} from '../data/data.js';
const App = () => {
const [showItems, setShowItems] = useState([]);
useEffect(() => {
const arr = [];
someData.map(obj => {
arr.push(false);
});
setShowItems(arr);
}, []);
const handlePressed = index => {
showItems[index] = true;
setShowItems(showItems);
//The list is changed.
//but the conditional rendering does not show the latest state
console.log(showItems);
};
return (
<View>
{someData.map((obj, index) => {
return (
<>
<Button
title={obj.title}
onPress={() => {
handlePressed(index);
}}
/>
{showItems[index] && <Text>{obj.item}</Text>}
</>
);
})}
</View>
);
};
This is because react is not identifying that your array has changed. Basically react will assign a reference to the array when you define it. But although you are changing the values inside the array, this reference won't be changed. Because of that component won't be re rendered.
And furthermore, you have to pass the key prop to the mapped button to get the best out of react, without re-rendering the whole button list. I just used trimmed string of your obj.title as the key. If you have any sort of unique id, you can use that in there.
So you have to notify react, that the array has updated.
import { someData } from "../data/data.js";
const App = () => {
const [showItems, setShowItems] = useState([]);
useEffect(() => {
const arr = [];
someData.map((obj) => {
arr.push(false);
});
setShowItems(arr);
}, []);
const handlePressed = (index) => {
setShowItems((prevState) => {
prevState[index] = true;
return [...prevState];
});
};
return (
<View>
{someData.map((obj, index) => {
return (
<>
<Button
key={obj.title.trim()}
title={obj.title}
onPress={() => {
handlePressed(index);
}}
/>
{showItems[index] && <Text>{obj.item}</Text>}
</>
);
})}
</View>
);
};
showItems[index] = true;
setShowItems(showItems);
React is designed with the assumption that state is immutable. When you call setShowItems, react does a === between the old state and the new, and sees that they are the same array. Therefore, it concludes that nothing has changed, and it does not rerender.
Instead of mutating the existing array, you need to make a new array:
const handlePressed = index => {
setShowItems(prev => {
const newState = [...prev];
newState[index] = true;
return newState;
});
}
If have a problem with react code where, for some reason, everything works as expected when I update the UI from the MyApp.promise().then(<here>) but not in my MyApp.promise().then().catch(<here>)
I know the code is actually executed up to the point I actually call setData which is my useState() returned function
A call to that function in then() works just fine, not in catch()
the exception that eventually triggers catch() works fine since the catch() is executed as expected
I added a console.log() inside my component, and I see that it's no longer re-drawn when the updates comes from catch()
I guess my question is : what would be special in a catch() function so react wouldn't behave ?
This is the code for my application hook that handles upgrade status updates :
const useUpdateStatus = () => {
const [data,setData] = useState({status: STATUS.IDLE,changelog:null,tasks:[]})
const updateData = (d) => {
// We call setData with an anonymous function so we can merge previous
// data with new data
setData((prev) => {
console.log({ ...prev, ...d })
return { ...prev, ...d }
})
};
// Only once, we set the timer to periodically update status
useEffect(() => {
setInterval(() => {
MyApp.get('/system/upgrade')
.then((upgrade) => {
// If anything is not "pending", it means we are upgrading
for (var t of upgrade.tasks) {
if (t.status !== "pending") {
updateData({ status: STATUS.INSTALLING})
}
}
// updateData will call setData with the full status
// This works as intended, UI is updated on each step
updateData({ tasks: upgrade.tasks, changelog: upgrade.changelog})
})
.catch((e) => {
// If data can't be fetched, it probably means we are restarting
// services, so we updated the tasks array accordingly
setData((prev) => {
for (var t of prev.tasks) {
if (t['id'] === "restarting") {
t['status'] = 'running'
}
else if (t['status'] == "running") {
t['status'] = 'finished'
}
}
// The expected data is logged here
console.log(prev)
return prev
})
})
}, 1000);
},[])
return data
}
This is the presentation layer :
// Using the hook :
const { changelog, tasks, status } = useUpdateStatus()
// Somewhere int he page :
<UpdateProgress tasks={tasks}/>
// The actual components :
const UpdateProgress = (props) => {
return(
<div style={{display: "flex", width: "100%"}}>
{ props.tasks.map(s => {
return(
<UpdateTask key={s.name} task={s}/>
)
})}
</div>
)
}
const UpdateTask = (props) => {
const colors = {
"pending":"LightGray",
"running":"SteelBlue",
"finished":"Green",
"failed":"red"
}
return(
<div style={{ textAlign: "center", flex: "1" }}>
<Check fill={colors[props.task.status]} width="50px" height="50px"/><br/>
<p style={props.task.status==="running" ? {fontWeight: 'bold'} : { fontWeight: 'normal'}}>{props.task.name}</p>
</div>
)
}
React performs an Object.is comparison to check is a re-render is needed after a state update call. Since you are mutating the state in catch block, react is falsely notified that the state hasn't changed and hence a re-render in not triggered
You can update your state like below to make it work
.catch((e) => {
// If data can't be fetched, it probably means we are restarting
// services, so we updated the tasks array accordingly
setData((prev) => {
for (var t of prev.tasks) {
if (t['id'] === "restarting") {
t['status'] = 'running'
}
else if (t['status'] == "running") {
t['status'] = 'finished'
}
}
// The expected data is logged here
console.log(prev)
return {...prev}
})
})
However a better way to update state is to do it in an immutable manner
.catch((e) => {
// If data can't be fetched, it probably means we are restarting
// services, so we updated the tasks array accordingly
setData((prev) => ({
...prev,
tasks: prev.tasks.map((task) => {
if (task.id === "restarting") {
return { ...task, status: 'running'}
}
else if (task.id === "running") {
return { ...task, status: 'finished'}
}
return task
})
}))
})
I have been trying to render information from my firebase to a react native component. I started by console logging what I have done, the data is being fetched completely fine:
displayAllPlayers(){
dbh.collection('Players').get()
.then(querySnapshot => {
querySnapshot.forEach(doc => {
console.log(doc.data().First, doc.data().Last)
})
})}
I then tried to add this information to my component as follows:
displayAllPlayers(){
dbh.collection('Players').get()
.then(querySnapshot => {
querySnapshot.forEach(doc => {
<Player key={doc.data().First} fName={doc.data().First} lName={doc.data().Last} />
})
})
}
render() {
const myPlayers = this.displayAllPlayers()
}
return(
{myPlayers}
)
It's always suggested to create a different helpers file.
Create a firebase-helpers.js file which has an function to convert snapshot to array.
// FILENAME - firebase-helpers.js
export const snapshotToArray = (snapshot) => {
let returnArr = [];
snapshot.forEach((childSnapshot) => {
let item = childSnapshot.data();
returnArr.push(item);
});
return returnArr;
};
Now in your screen, import this file
import { snapshotToArray } from "../helpers/firebaseHelpers";
Then, convert snapshot of Players to array
const playersSnapshot = dbh.collection('Players').get();
const playersArray = snapshotToArray(playersSnapshot);
this.setState({ players : playersArray });
Now in state you have an array players. To display content of Players, you can use in your render function as -
<FlatList
data={this.state.players}
renderItem={({ item }, index) => this.playerDisplay(item, index)}
keyExtractor={(item, index) => index.toString()}
/>
You can then have a function to return details of players as -
playerDisplay = (item, index) => {
return(
<View>
<Text>
Player {index} - {item.First} {item.Last}
</Text>
</View>
);
}
I hope it works fine.
You should return the JSX inside the render function.
displayAllPlayers isn't returning anything.
In this snippet
querySnapshot.forEach(doc => {
<Player key={doc.data().First} fName={doc.data().First} lName={doc.data().Last} />
})
you're not returning anything inside the callback passed to forEach even if you do, it doesn't work because forEach doesn't return an array. You can use map here.
Maintain a state in the component and update it once you get the data. Use this state for rendering the array of JSX elements.
I'm trying to create my own pagination (without using a package), but I can't get it to work.
I'm wondering if it has something to do with how I'm copying my arrays, but I'm not really sure.
class InsightSearchResults extends Component {
state = {
start: 0,
end: 2,
insightsArrayOriginal: [],
copiedArr: []
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.insightList[0]) {
this.setState({
insightsArrayOriginal: nextProps.insightList[0].insights,
copiedArr: nextProps.insightList[0].insights.splice(this.state.start, this.state.end)
})
}
}
clickNext = () => {
let copied = [...this.state.insightsArrayOriginal];
this.setState({
start: this.state.start + 2,
end: this.state.end + 2
}, () => {
this.setState({
copiedArr: copied.splice(this.state.start, this.state.end)
})
})
}
clickPrev = () => {
this.setState({
start: this.state.start - 2 < 0 ? 0 : this.state.start - 2,
end: this.state.end - 2
})
}
render() {
const { copiedArr } = this.state;
return (
<div style={{padding: "1.5rem"}}>
{copiedArr ? copiedArr.map(insight => (
<div>
<Grid className="insight_result_block">
<Col className="insight_results_col2" span="10">
<div>
<h4>Hello</h4>
<p>{insight.insightDesc}</p>
</div>
</Col>
</Grid>
<hr className="bottom_hr_insight" />
</div>
)) : <p>loading...</p> }
<button onClick={this.clickPrev}>Prev</button>
<button onClick={this.clickNext}>Next</button>
</div>
)
}
}
I haven't really worked on the "prev" part yet. I'm just trying to get the "next" to work for now...
There are two problems:
UNSAFE_componentWillReceiveProps is not called on initial render. From the docs:
React doesn’t call UNSAFE_componentWillReceiveProps() with initial
props during mounting. It only calls this method if some of
component’s props may update. Calling this.setState() generally
doesn’t trigger UNSAFE_componentWillReceiveProps().
splice mutates the original array, use slice instead. See this question.
So you can move the content of UNSAFE_componentWillReceiveProps to componentDidMount and componentDidUpdate
componentDidMount() {
this.updateState();
}
componentDidUpdate() {
// check if a change in props has caused the rerender
// or you will get infinite rerenders if one state update causes the next one
if (
this.props.insightList[0] &&
this.props.insightList[0].insights !== this.state.insightsArrayOriginal
) {
this.updateState();
}
}
These functions don't receive a parameter: replace nextProps parameter with this.props; and change all splice occurrences with slice.
updateState() {
if (this.props.insightList[0]) {
this.setState({
insightsArrayOriginal: this.props.insightList[0].insights,
copiedArr: this.props.insightList[0].insights.slice( . // <-here
this.state.start,
this.state.end
)
});
}
}
clickNext = () => {
let copied = [...this.state.insightsArrayOriginal];
this.setState({ start: this.state.start + 2, end: this.state.end + 2 },
() => {
this.setState({
copiedArr: copied.slice(this.state.start, this.state.end) // <- and here
});
}
);
};
Also, based on this code sample alone, you could entirely remove insightsArrayOriginal from your state and use it from props, but this may change if you plan to expand the functionality.
I'm trying to create a reference using the useRef hook for each items within an array of `data by doing the following:
const markerRef = useRef(data.posts.map(React.createRef))
Now, data is fetched externally through GraphQL and it takes time to arrive, therefore, during the mounting phase, data is undefined. This causes the following error:
TypeError: Cannot read property '0' of undefined
I've tried the following with no success:
const markerRef = useRef(data && data.posts.map(React.createRef))
How do I set up so that I can map through the data without causing the error?
useEffect(() => {
handleSubmit(navigation.getParam('searchTerm', 'default value'))
}, [])
const [loadItems, { called, loading, error, data }] = useLazyQuery(GET_ITEMS)
const markerRef = useRef(data && data.posts.map(React.createRef))
const onRegionChangeComplete = newRegion => {
setRegion(newRegion)
}
const handleSubmit = () => {
loadItems({
variables: {
query: search
}
})
}
const handleShowCallout = index => {
//handle logic
}
if (called && loading) {
return (
<View style={[styles.container, styles.horizontal]}>
<ActivityIndicator size="large" color="#0000ff" />
</View>
)
}
if (error) return <Text>Error...</Text>
return (
<View style={styles.container}>
<MapView
style={{ flex: 1 }}
region={region}
onRegionChangeComplete={onRegionChangeComplete}
>
{data && data.posts.map((marker, index) => (
<Marker
ref={markerRef.current[index]}
key={marker.id}
coordinate={{latitude: marker.latitude, longitude: marker.longitude }}
// title={marker.title}
// description={JSON.stringify(marker.price)}
>
<Callout onPress={() => handleShowCallout(index)}>
<Text>{marker.title}</Text>
<Text>{JSON.stringify(marker.price)}</Text>
</Callout>
</Marker>
))}
</MapView>
</View>
)
I'm using the useLazyQuery because I need to trigger it at different times.
Update:
I have modified the useRef to the following on the advise of #azundo:
const dataRef = useRef(data);
const markerRef = useRef([]);
if (data && data !== dataRef.current) {
markerRef.current = data.posts.map(React.createRef);
dataRef.current = data
}
When I console.log markerRef.current, I get the following result:
which is perfectly fine. However, when I attempt to map each current and invoke showCallout() to open all the callouts for each marker by doing the following:
markerRef.current.map(ref => ref.current && ref.current.showCallout())
nothing gets executed.
console.log(markerRef.current.map(ref => ref.current && ref.current.showCallout()))
This shows null for each array.
The useRef expression is only executed once per component mount so you'll need to update the refs whenever data changes. At first I suggested useEffect but it runs too late so the refs are not created on first render. Using a second ref to check to see if data changes in order to regenerate the marker refs synchronously should work instead.
const dataRef = useRef(data);
const markerRef = useRef([]);
if (data && data !== dataRef.current) {
markerRef.current = data.posts.map(React.createRef);
dataRef.current = data;
}
Additional edit:
In order to fire the showCallout on all of the components on mount, the refs must populated first. This might be an appropriate time for useLayoutEffect so that it runs immediately after the markers are rendered and ref values (should?) be set.
useLayoutEffect(() => {
if (data) {
markerRef.current.map(ref => ref.current && ref.current.showCallout());
}
}, [data]);
Create refs using memoisation, like:
const markerRefs = useMemo(() => data && data.posts.map(d => React.createRef()), [data]);
Then render them like:
{data &&
data.posts.map((d, i) => (
<Marker key={d} data={d} ref={markerRefs[i]}>
<div>Callout</div>
</Marker>
))}
And use the refs for calling imperative functions like:
const showAllCallouts = () => {
markerRefs.map(r => r.current.showCallout());
};
See the working code with mocked Marker: https://codesandbox.io/s/muddy-bush-gfd82