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
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;
});
}
let [item, setItem] = useState({});
let [comments, setComments] = useState([]);
useEffect(async () => {
await axios
.all([
axios.get(`https://dummyapi.io/data/v1/post/${id}`, {
headers: { "app-id": process.env.REACT_APP_API_KEY }
}),
axios.get(`https://dummyapi.io/data/v1/post/${id}/comment`, {
headers: { "app-id": process.env.REACT_APP_API_KEY }
})
])
.then(
axios.spread((detail, comment) => {
setItem({ ...detail.data })
setComments([...comment.data.data])
})
)
.catch((detail_err, comment_err) => {
console.error(detail_err);
console.error(comment_err);
});
}, []);
i setStated like above.
and I was trying to use the State in return(), but it seems it didn't wait for the data set.
return (
<div>
{item.tags.map((tag, index) => {
return <Chip label={tag} key={index} />
})}
</div>
)
because i got an error message like this : Uncaught TypeError: Cannot read properties of undefined (reading 'map').
Since i initialized 'item' just empty {object}, so it can't read 'item.tags', which is set by setState in useEffect.
How can i wait for the data set?
In generic, it would set a state isFetched to determine if the data from api is ready or not. And when the isFetched equal to true, it means the item.tags have value.
const [isFetched, setIsFetched] = useState(false);
useEffect(async () => {
await axios.all(...).then(() => {
...
...
setIsFetched(true);
})
}, [])
// You could return null or an Loader component meaning the api is not ready
if (!isFetched) return null;
return (
<div>
{item.tags.map((tag, index) => {
return <Chip label={tag} key={index} />
})}
</div>
)
On the other hand, you could use optional chaining to avoid using map from an undefined value (that is item.tags), the right way is replace item.tags.map to item.tags?.map.
Initially, item is an empty JSON ({}). You should be using the optional chaining operator(?.) to easily get rid of the null or undefined exceptions.
return (
<div>
{item?.tags?.map((tag, index) => {
return <Chip label={tag} key={index} />
})}
</div>
)
let [item, setItem] = useState({});
Your initial state is an empty object, and there will always be at least one render that uses this initial state. Your code thus needs to be able to work correctly when it has this state. For example, you could check if item.tags exists before you try to use it:
if (item.tags) {
return (
<div>
{item.tags.map((tag, index) => {
return <Chip label={tag} key={index] />
})}
</div>
);
} else {
return <div>Loading...</div>
}
Alternatively, you could change your initial state so it has the same shape that it will have once loading has finished:
let [item, setItem] = useState({ tags: [] });
I wanna implement a live Search function on my Redux State which I use in my home page via useSelector. and when user delete the search content original data show up as well. I use filter but the data doesn't affect. how can I achieve that? any help would be appreciated:
const Home = (props) => {
const companies = useSelector(state => state.companies.availableCompanies); //this is my data
const handleSearch = (e) => {
companies.filter(el => el.name.includes(e));
console.log(companies) // here I see my data changes but doesn't affect on UI
}
return (
<SearchBar onChangeText={handleSearch} />
<View style={styles.cardContainer}> // here I show data.
{companies.map((el, index) => {
return (
<Card
key={el.id}
companyId={el.id}
companyName={el.name}
companyImage={el.image}
companyMainAddress={el.mainAddress}
companyPhoneNumber={el.telephoneNumber}
companyDetails={el.details}
onPress={() => {
navigation.navigate('CardDetails', {
id: el.id,
companyName: el.name,
});
}}
/>
)
})}
</View>
Have a try with the below changes
Hope it will work for you.
const Home = (props) => {
const [searchQuery, setSearchQuery] = useState();
const [filteredData, setFilteredData] = useState();
const companies = useSelector(state => state.companies.availableCompanies); //this is my data
const handleSearch = (e) => {
setSearchQuery(e);
}
useEffect(() => {
if (searchQuery && typeof searchQuery === "string" && searchQuery.length > 0) {
const searchData = companies.filter(el => el.name.includes(searchQuery));
setFilteredData([...searchData]);
} else {
setFilteredData();
}
}, [searchQuery, companies])
return (
<SearchBar onChangeText={handleSearch} />
<View style={styles.cardContainer}>
{(filteredData && Array.isArray(filteredData) ? filteredData : companies).map((el, index) => {
return (
<Card
key={el.id}
companyId={el.id}
companyName={el.name}
companyImage={el.image}
companyMainAddress={el.mainAddress}
companyPhoneNumber={el.telephoneNumber}
companyDetails={el.details}
onPress={() => {
navigation.navigate('CardDetails', {
id: el.id,
companyName: el.name,
});
}}
/>
)
})}
</View>
This variable below is a copy of state.companies.availableCompanies that is replaced on every render with the original value from state.companies.availableCompanies.
const companies = useSelector(state => state.companies.availableCompanies); //this is my data
Since you're assigning the result of filter to the copy, and not to the original variable inside the redux store. The results are not reflected there, and every time rerender happens, the Functional Component is called again, making all the code inside this function execute again. So, there is a new variable companies that is not related to the old one.
To actually update the original variable inside redux. You need to create a redux action, and dispatch it.
You need to go back and learn the fundamental concepts of redux before proceeding with this.
Here is the link to the documentation explaining how the data flow works in redux.
https://redux.js.org/tutorials/fundamentals/part-2-concepts-data-flow
You need to use useDispatch() to get dispatcher and dispatch an action to your reducer with state to update in your handleSearch ()
Something like:
const dispatch = useDispatch();
const handleSearch = (e) => {
dispatch({type:"YOUR_ACTION",payload:companies.filter(el => el.name.includes(e))})
console.log(companies) ;
}
Refer: https://medium.com/#mendes.develop/introduction-on-react-redux-using-hooks-useselector-usedispatch-ef843f1c2561
I want to scroll FlatList to the certain index when screen is launched and data for the list is retrieved from the server.
I have a problem of using ref inside useMemo(). I'm getting an error:
Cannot read property current of undefined.
How to fix this error? Does my approach correct?
Here is what I'm doing:
const WeeklyMeetings = props => {
const [meetings, setMeetings] = useState(null)
useEffect(() => {
AllMeeting() // getting data from the server
}, [])
const getResult = useMemo(() => {
flatListRef.current.scrollToIndex({index: 15, animated: true })
}, [meetings]);
const flatListRef = useRef();
const AllMeeting = async (id) => {
setLoading(true)
try {
const meetings = await meeting.allMeetingsAsc(id)
setMeetings(meetings)
} catch (error) {
console.log("error ", error)
}
}
return (
<View style={styles.rootContainer}>
<FlatList
ref={flatListRef}
style={styles.list}
data={meetings}
renderItem={renderItem}
onScrollToIndexFailed={()=>{}}
/>
</View>
)
}
The ref needs to be defined before using it.
Also since you want to just scroll to index when you receive meeting value, you can make use of useEffect hook.
Also note that you only want to scrollToIndex once value meetings is available and hence you can skip the initial call to useEffect by keeping track of initialRender
const WeeklyMeetings = props => {
const [meetings, setMeetings] = useState(null)
useEffect(() => {
const AllMeeting = async (id) => {
setLoading(true)
try {
const meetings = await meeting.allMeetingsAsc(id)
setMeetings(meetings)
} catch (error) {
console.log("error ", error)
}
}
AllMeeting();
}, [])
const flatListRef = useRef();
const initialRender = useRef(true)
useEffect(() => {
if(!initialRender.current) {
flatListRef.current.scrollToIndex({index: 15, animated: true })
} else {
initialRender.current = false;
}
}, [meetings])
return (
<View style={styles.rootContainer}>
<FlatList
ref={flatListRef}
style={styles.list}
data={meetings}
renderItem={renderItem}
getItemLayout={(data, index) => (
{length: 50, offset: 50 * index, index}
)}
/>
</View>
)
}
According to Documentation
You would need to implement a getItemLayout function for FlatList
since scrollToIndex Cannot scroll to locations outside the render
window without specifying the getItemLayout prop.
I'm trying to display list of events based on the search query dynamically.
The problem is that I'm always on the initial View and the renderSearch View is never called.
PastEvent is a function called from the primary redner of the class by scenemap
Please check comments in the code.
//to display the past events tab
PastEvents = () => {
const state = this.state;
let myTableData = [];
if (
state.PastEventList.length !== 0
) {
state.PastEventList.map((rowData) =>
myTableData.push([
this.renderRow(rowData)
])
);
}
function renderPast() {
console.log("im in render past") //shows
return (
<ScrollView horizontal={false}>
<Table style={styles.table}>
{myTableData.map((rowData, index) => (
<Row
key={index}
data={rowData}
style={styles.row}
textStyle={styles.rowText}
widthArr={state.widthArr}
/>
))}
</Table>
</ScrollView>
)
}
function renderSearch() {
console.log("im in render search") //never shows even after changing the text
let searchTable = [];
if (
this.state.seacrhPastList.length !== 0
) {
state.seacrhPastList.map((rowData) =>
searchTable.push([
this.renderRow(rowData)
])
);
}
return (
<ScrollView horizontal={false}>
<Table style={styles.table}>
{searchTable.map((rowData, index) => (
<Row
key={index}
data={rowData}
style={styles.row}
textStyle={styles.rowText}
widthArr={state.widthArr}
/>
))}
</Table>
</ScrollView>
)
}
return (
<View style={styles.container}>
<TextInput placeholder="Search for Events" onChangeText={text => this.onChangeSearch(text)}></TextInput>
{this.state.searching ? renderSearch() : renderPast()} //please check the onchangeSearch function
</View>
)
}
And the function of change is like that:
onChangeSearch = (text) => {
if (text.length > 0) {
let jsonData = {};
//get list of events
let url = "/api/FindEvents/" + text.toLowerCase()
ApiHelper.createApiRequest(url, jsonData, true).then(res => {
if (res.status == 200) {
this.state.seacrhPastList = res.data
this.state.searching= true //I was hoping this change will cause the render
}
})
.catch(err => {
console.log(err);
return err;
});
}
}
How can i change the events based on the query of the input ? Thank you
you need to use useState here
declare useState like this:
PastEvents = () => {
const [searching, setText] = useState(false);
change the searching state here:
if (res.status == 200) {
this.state.seacrhPastList = res.data
setText(true);
}
Hope this helps!
You're in a stateless component you shouldn't use "this" in any way, also you can't use state that way, you need to use react hooks
Import { useState } from 'react'
Then you can use state in a functional component
const [state, setState] = useState(initialvalue);