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.
Related
I'm creating a React native app for a covid form
I'm getting the data from a Json so I've used fetch and .then to get the actually json with the question and save it on a QuestionsState, but when I try to filter the data and get the individual question to save that on a QuestionState, that is not showing on the view, but when I console.log that and press ctrl + s to save en vs code, the individual question appears
the questions code is this
import { StyleSheet, Text, View, Button } from 'react-native'
import React, {useState, useEffect} from 'react'
const url = 'https://raw.githubusercontent.com/alejoduke52/covidForm/main/covid-19.json'
const Questions = () => {
const [covidQuestions, setCovidQuestions] = useState([{}])
const [individualQuestion, setIndividualQuestion] = useState([])
const [loading, setLoading] = useState(true)
const [countId,setCountId] = useState(1)
useEffect(() => {
console.log("cargando use effect")
getData()
},[])
useEffect(() => {
console.log("cargando use effect data unica")
getQuestion(1)
},[countId])
const getData = () =>{
fetch(url)
.then(resp => resp.json())
.then(data => {
setCovidQuestions(data)
getQuestion(1)
})
}
const getQuestion = (id) => {
setIndividualQuestion(covidQuestions.filter(Question => Question.id === id))
console.log(individualQuestion)
setLoading(false)
}
const handlerNextQuestion = () => {
console.log("jasdjasjdas")
console.log(individualQuestion)
//getQuestion(countId)
setCountId(countId+1)
}
return (
<View>
{
loading ? <Text style={styles.loading}>LOADING</Text> : (
<View key={0}>
<Text>hello {individualQuestion.question}</Text>
</View>
)
//jsx
}
<Button title="Cargando s" onPress={() => handlerNextQuestion()} />
</View>
);
}
export default Questions
const styles = StyleSheet.create({
loading: {
fontSize: 40
}
})
It is very strange cause when I first render the app the state is empty but only when I press ctrl + s, it shows
individualQuestion state is an array you have to use it like
Option 1:
hello {individualQuestion[0].question}
Option 2:
setIndividualQuestion(covidQuestions.filter(Question => Question.id === id)[0])
You can use getQuestion(1, data) instead of getQuestion(1).
You can pass data to getQuestion function
const getData = () =>{
fetch(url)
.then(resp => resp.json())
.then(data => {
setCovidQuestions(data)
getQuestion(1, data)
})
}
const getQuestion = (id,data) => {
setIndividualQuestion(data.filter(Question => Question.id === id))
...
...
...
}
Please check the below code It will give you full functionality:
import { StyleSheet, Text, View, Button } from 'react-native'
import React, { useState, useEffect } from 'react'
const url = 'https://raw.githubusercontent.com/alejoduke52/covidForm/main/covid-19.json'
const Questions = () => {
const [covidQuestions, setCovidQuestions] = useState([])
const [individualQuestion, setIndividualQuestion] = useState()
const [loading, setLoading] = useState(true)
const [countId, setCountId] = useState(1)
useEffect(() => {
console.log("cargando use effect")
getData()
}, [])
const getData = () => {
fetch(url)
.then(resp => resp.json())
.then(data => {
if (data.length > 0) {
setCovidQuestions(data)
console.log("Data", data)
getQuestion(1, data)
}
})
}
const getQuestion = (id, data) => {
console.log(data)
let question = data.find(Question => Question.id === id)
console.log("Question", question)
setIndividualQuestion(question)
setLoading(false)
}
const handlerNextQuestion = () => {
setCountId(countId + 1)
getQuestion(countId + 1, covidQuestions)
}
return (
<View>
{
loading ? <Text style={styles.loading}>LOADING</Text> : (
<View key={0}>
<Text>hellO</Text>
{individualQuestion != undefined ?
<View>
<Text>{JSON.stringify(individualQuestion)}</Text>
<Text>Question: {individualQuestion.question}</Text>
</View> : null}
</View>
)
}
<Button title="next" onPress={() => handlerNextQuestion()} />
</View>
);
}
export default Questions
const styles = StyleSheet.create({
loading: {
fontSize: 40
}
})
Hope You will like it! :)
Here there are two problem which I see
First problem is you are trying to access covidQuestions immediately after setCovidQuestions inside getQuestion(1) function, since setting state in react is always an asynchronous event and takes some time to update the state value. So basically you should not call getQuestion(1) immediately after setCovidQuestions .
Second, problem I see with the filter you are using, since filter always return an array of filtered questions, even if there is one question, so you can never access individualQuestion like {individualQuestion.question}, since its an array.
My solution to the first problem would be to remove getQuestion(1) after setCovidQuestions inside fetch API call and instead you can actually add covidQuestions as dependency to your useEffect which runs on change of countId like below
JSX
useEffect(() => {
console.log("cargando use effect data unica")
getQuestion(1)
},[countId, covidQuestions])
And for your second problem related to filter mechanism, you can do something like this, while rendering
JSX
<View key={0}>
<Text>hello {individualQuestion[0].question}</Text>
</View>
I am building a image slider in React, based on CSS vertical snapping. There are 2 ways to interact with it, either throught scroll vertically or click the navigation buttons. I am using the Intersection Observer API in a React useEffect() to detect the active item. However, I can't seem to get it right without any useEffect lint errors. Whenever I include the functions in the dependecy array as suggested by the lint, the active item isn't set when scrolling.
Am I using a React anti pattern or am I just missing something?
Live demo
Code:
const Slider = ({images}) => {
const [currentSlide, SetCurrentSlide] = React.useState(0);
const setSlide = (id) => {
SetCurrentSlide(id);
};
const moveToSlide = (id) => {
if(id > -1 && id < images.length) {
SetCurrentSlide(id);
}
}
return (
<StyledSlider id="slider">
<SliderWrapper items={images} setSlide={setSlide} currentSlide={currentSlide} />
<SliderNav currentSlide={currentSlide} moveToSlide={moveToSlide} maxItems={images.length}/>
</StyledSlider>
)
}
const SliderWrapper = ({items, setSlide, currentSlide}) => {
const containerRef = React.useRef(null);
const { ref, inView, entry } = useInView({
/* Optional options */
threshold: 0,
});
const handleSetSlide = (id) => {
setSlide(id);
};
const handleIntersection = (entries) => {
const [entry] = entries;
const activeSlide = Number(entry.target.dataset.slide);
if (!entry.isIntersecting || activeSlide === "NaN") return;
handleSetSlide(activeSlide);
};
React.useEffect(() => {
const observer = new IntersectionObserver(
handleIntersection,
{
root: containerRef.current,
threshold: 0.45
}
);
Array.from(containerRef.current.children).forEach((item) => {
observer.observe(item);
});
return function() {
observer.disconnect();
}
}, [items]);
return (
<StyledSliderWrapper ref={containerRef} >
{items.map((item, index) => {
return <SliderItem key={index} index={index} image={item} isActive={currentSlide === index} />
})}
</StyledSliderWrapper>
)
};
const SliderItem = ({index, image, isActive}) => {
const imageContent = getImage(image.url);
const imageRef = React.useRef()
React.useEffect(() => {
if(!isActive) return;
imageRef.current.scrollIntoView({behavior: "smooth", block: "center", inline: "center"});
},[isActive]);
return (
<StyledSliderItem data-slide={index} ref={imageRef}>
<GatsbyImage image={imageContent} alt={image.description} />
</StyledSliderItem>
)
}
So you've missing dependencies in the useEffect of SliderWrapper. You can simplify the code a bit as well.
SliderWrapper
Since nothing else calls handleIntersection callback other than the Observer you can safely move it into the useEffect callback body. This makes the only dependency the setSlide callback that's passed as a prop from the parent component.
const SliderWrapper = ({ items, setSlide, currentSlide }) => {
const containerRef = React.useRef(null);
React.useEffect(() => {
const handleIntersection = (entries) => {
const [entry] = entries;
const activeSlide = Number(entry.target.dataset.slide);
if (!entry.isIntersecting || activeSlide === "NaN") return;
setSlide(activeSlide);
};
const observer = new IntersectionObserver(handleIntersection, {
root: containerRef.current,
threshold: 0.45
});
Array.from(containerRef.current.children).forEach((item) => {
observer.observe(item);
});
return function () {
observer.disconnect();
};
}, [setSlide]);
return (
<StyledSliderWrapper ref={containerRef}>
{items.map((item, index) => (
<SliderItem
key={index}
index={index}
image={item}
isActive={currentSlide === index}
/>
))}
</StyledSliderWrapper>
);
};
Slider
The other issue what that you were memoizing the setSlide prop in the child instead of the parent where it's being passed down. This caused the setSlide prop to be a new reference each render and re-memoized via useCallback in the child. React useState updater functions are stable however, so you can directly pass them to children.
const Slider = ({ images }) => {
const [currentSlide, setCurrentSlide] = React.useState(0);
const moveToSlide = (id) => {
setCurrentSlide(id);
};
return (
<StyledSlider id="slider">
<SliderWrapper
items={images}
setSlide={setCurrentSlide} // <-- pass directly to child
currentSlide={currentSlide}
/>
<SliderNav
currentSlide={currentSlide}
moveToSlide={moveToSlide}
maxItems={images.length}
/>
</StyledSlider>
);
};
If you wanted to remain with the setSlide handler in the parent, here is where you'd memoize the callback so the parent is providing a stable reference. Note that this is only useful if memoizing non-useState functions.
const setSlide = React.useCallback(
(id) => {
setCurrentSlide(id);
},
[setCurrentSlide]
);
I try to save the tasks in my ToDo app with AsyncStorage so that they can be retrieved after an app restart.
So far I have managed to save the tasks. However, an empty array is always saved in the first run. If I want to create a new task, it only saves it the second time I click the button. Logical if the whole thing runs asynchronously. I just can't figure out where my fault is. I would be very happy to receive help and tips.
Here you can see the empty array when creating the first task:
Reactotron Empty Array
And here you can see the first value get's saved after i created the second task:
Reactotron AsyncStorage after second click
First Part:
if (__DEV__) {
import('./ReactotronConfig').then(() => console.log('Reactotron Configured'));
}
import Reactotron, { asyncStorage } from 'reactotron-react-native';
import React, { useState, useEffect } from 'react';
import {
Keyboard,
KeyboardAvoidingView,
Platform,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
ScrollView,
Image,
SafeAreaView,
} from 'react-native';
import AsyncStorage from '#react-native-async-storage/async-storage';
import Task from './components/Task';
export default function App() {
const [task, setTask] = useState();
const [taskItems, setTaskItems] = useState([]);
const getData = async () => {
try {
const jsonValue = await AsyncStorage.getItem('TASKS');
const jsonValue2 = JSON.parse(jsonValue);
if (jsonValue2 !== null) {
setTaskItems(jsonValue2);
}
} catch (e) {
alert(e);
}
};
const storeData = async () => {
await AsyncStorage.clear();
try {
const jsonValue = await AsyncStorage.setItem(
'TASKS',
JSON.stringify(taskItems)
);
return jsonValue;
Reactotron.log(jsonValue);
} catch (e) {
alert(e);
}
};
useEffect(() => {
getData();
}, []);
const handleAddTask = () => {
storeData();
Keyboard.dismiss();
setTaskItems([...taskItems, task]);
setTask(null);
};
const completeTask = (index) => {
let itemsCopy = [...taskItems];
itemsCopy.splice(index, 1);
setTaskItems(itemsCopy);
};
const bearyDustLogo = require('./assets/bearydust-logo-bear-with-text.png');
Second Part:
return (
<SafeAreaView style={styles.container}>
<ScrollView style={styles.scrollView}>
{/* Aufgaben für heute */}
<View style={styles.tasksWrapper}>
<View style={styles.headerWrapper}>
<Text style={styles.sectionTitle}>StandUp Aufgaben</Text>
<Image style={styles.tinyLogo} source={bearyDustLogo}></Image>
</View>
<View style={styles.items}>
{/* Aufgabenbereich */}
{taskItems.map((item, index) => {
return (
<TouchableOpacity
key={index}
onPress={() => completeTask(index)}
>
<Task text={item} />
</TouchableOpacity>
);
})}
</View>
</View>
</ScrollView>
{/* Aufgabe erstellen */}
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.writeTaskWrapper}
>
<TextInput
style={styles.input}
placeholder={'Ey! Lass was machen'}
value={task}
onChangeText={(text) => setTask(text)}
/>
<TouchableOpacity
onPress={() => {
handleAddTask();
}}
>
<View style={styles.addWrapper}>
<Text style={styles.addText}>+</Text>
</View>
</TouchableOpacity>
</KeyboardAvoidingView>
</SafeAreaView>
);
}
Looking at the code, it's first saving and then it's updating the array, so you will always be one step behind on your storage:
const handleAddTask = () => {
storeData(); // here you are handling the save
Keyboard.dismiss();
setTaskItems([...taskItems, task]); // but you update the values only here
setTask(null);
};
To keep your code simple I would suggest you to save each time you have an update on your taskItems but keep in mind you don't need to update the first time you load from storage, so something like this should work:
export default function App() {
const [task, setTask] = useState();
const [taskItems, setTaskItems] = useState([]);
const [loading, setLoading] = useState(true);
const getData = async () => {
try {
const jsonValue = await AsyncStorage.getItem('TASKS');
const jsonValue2 = JSON.parse(jsonValue);
if (jsonValue2 !== null) {
setTaskItems(jsonValue2);
}
} catch (e) {
alert(e);
} finally {
setLoading(false)
}
};
const storeData = async () => {
// commenting this line as you don't need to clean the storage each time you write something on it, as you'll just override the existing key 'TASKS'
// await AsyncStorage.clear();
// here you validate if the data was loaded already before starting watching for changes on the list
if (!loading) {
try {
const jsonValue = await AsyncStorage.setItem(
'TASKS',
JSON.stringify(taskItems)
);
return jsonValue;
Reactotron.log(jsonValue);
} catch (e) {
alert(e);
}
}
}
useEffect(() => {
getData();
}, []);
useEffect(() => {
storeData()
}, [taskItems])
const handleAddTask = () => {
// won't need this line anymore as now everything you update your list it will be saved.
// storeData();
Keyboard.dismiss();
setTaskItems([...taskItems, task]);
setTask(null);
};
const completeTask = (index) => {
let itemsCopy = [...taskItems];
itemsCopy.splice(index, 1);
// this will trigger a save as well
setTaskItems(itemsCopy);
};
const bearyDustLogo = require('./assets/bearydust-logo-bear-with-text.png');
This way you will always save any updates on the task list and will prevent to save then as empty when first rendering your component.
Success on your project.
I'm getting the data from the database and show it in a FlatList. Whenever I add or remove something from the data the data isn't showing correctly in the FlatList.
Whenever I remove something it shows an empty list.
Whenever I add something it only shows the newly added data - nothing else.
I'm using firebase realtime database and use the data I get as follows:
firebase.database().ref(`/wordlists/${editKey}`).on('value', snap => {
if (snap.val() !== null) {
setIsLoading(false);
const val = snap.val().words;
const data = [];
Object.keys(val).forEach(key => {
data.push({ key, word: val[key].word });
})
setWords(data);
// setWords([...data]) doesn't work either.
}
})
My Flatlist looks like this:
<FlatList
data={words}
renderItem={renderItem}
keyExtractor={item => item.key}
extraData={words}
/>
When I console.log() the data I always get the data I want to show but the FlatList just won't show it correctly.
It also doesn't work when I use the spread-operator and/or extraData.
Because someone asked for it here is the entire file (I left out the styling and the imports)
const EditList = ({ editKey }) => {
const [wordlist, setWordlist] = useState(0);
const [refresh, setRefresh] = useState(false);
const [words, setWords] = useState([]);
const [wordLoading, setWordLoading] = useState({ loading: false });
const [loading, setIsLoading] = useState(false);
const [btnLoading, setBtnLoading] = useState(false);
const [word, setWord] = useState('');
useEffect(() => {
if (editKey !== 0) {
setIsLoading(true);
firebase.database().ref(`/wordlists/${editKey}`).on('value', snap => {
if (snap.val() !== null) {
setIsLoading(false);
setWordlist({...snap.val()});
const val = snap.val().words;
const data = [];
Object.keys(val).forEach(key => {
data.push({ key, word: val[key].word });
})
setWords([...data]);
setRefresh(!refresh);
console.log(data, 'DATA');
}
})
}
}, [editKey])
const onAdd = () => {
setBtnLoading(true);
firebase.database().ref(`/wordlists/${editKey}/words`).push({ word })
.then(() => {
setBtnLoading(false);
setWord('');
setRefresh(!refresh);
})
}
const onDelete = (key) => {
setWordLoading({ key, loading: true });
firebase.database().ref(`/wordlists/${editKey}/words/${key}`).remove().then(() => {
setWordLoading({ loading: false });
setRefresh(!refresh);
});
}
const renderItem = ({ item }) => (
<ItemWrapper>
<ItemWord>{ item.word }</ItemWord>
<DeleteView onPress={() => onDelete(item.key)}>
{ wordLoading.loading && wordLoading.key === item.key ?
<ActivityIndicator size="small" /> :
<DIcon name="trash-2" size={24} />
}
</DeleteView>
</ItemWrapper>
)
const createData = (words) => {
const data = [];
if (typeof words !== 'undefined') {
Object.keys(words).forEach(key => {
const obj = { key, word: words[key].word };
data.push(obj);
})
}
console.log(data, 'DATADATADATA');
return data;
}
if (editKey === 0) {
return (
<NokeyWrapper>
<NoKeyText>No list selected...</NoKeyText>
</NokeyWrapper>
)
}
if (loading) {
return (
<NokeyWrapper>
<ActivityIndicator size="large" />
</NokeyWrapper>
)
}
return (
<Wrapper
behavior={Platform.OS == "ios" ? "padding" : "height"}
keyboardVerticalOffset={Platform.OS === 'ios' && 180}
>
<WordListName>{wordlist.listName}</WordListName>
<FlatListWrapper>
<FlatList
data={words}
renderItem={renderItem}
keyExtractor={item => item.key}
//extraData={refresh}
extraData={words}
/>
</FlatListWrapper>
<AddWordWrapper>
<SInput value={word} onChangeText={(text) => setWord(text)} />
<Button onPress={() => onAdd()} loading={btnLoading}>
<Feather name="plus" size={24} color="black" />
</Button>
</AddWordWrapper>
</Wrapper>
)
};
export default EditList;
u need to useRef for this instance because the new 'words' is not inside the .on('value') call.
const [words, _setWords] = useState([]);
const wordRef = useRef(words)
//function to update both wordRef and words state
const setWords = (word) => {
wordRef = word
_setWords(word)
}
useEffect(() => {
if (editKey !== 0) {
setIsLoading(true);
let data = wordRef //create a temp data variable
firebase.database().ref(`/wordlists/${editKey}`).on('value', snap => {
if (snap.val() !== null) {
setIsLoading(false);
setWordlist({...snap.val()});
const val = snap.val().words;
Object.keys(val).forEach(key => {
data.push({ key, word: val[key].word });
})
setWords(data);
setRefresh(!refresh);
console.log(data, 'DATA');
}
})
return () => firebase.database().ref(`/wordlists/${editKey}`).off('value') // <-- need to turn it off.
}
}, [editKey, wordRef])
You probably need to change setRefresh etc with the same method if they are not refreshing.
After a lot more tries I found out the problem was somewhere else. Somehow using 'flex: 1' on my in my renderItem() was causing this issue. I actually found this issue also on github: GitHub React Native issues
So after removing 'flex: 1' from the element everything was showing as expected.
// before
const renderItem = ({ item }) => (
<ItemWrapper style={{ flex: 1, flexDirection: row }}>
<ItemWord>{ item.word }</ItemWord>
</ItemWrapper>
)
// after
const renderItem = ({ item }) => (
<ItemWrapper style={{ width: '100%' }}>
<ItemWord>{ item.word }</ItemWord>
</ItemWrapper>
)
I'm trying to translate a Class Component into a Functional one with React Native.
My Search component lets the user search for a film name and I'm making an API call to show him all corresponding films.
Here is my class component :
class Search extends React.Component {
constructor(props) {
super(props);
this.searchedText = "";
this.page = 0
this.totalPages = 0
this.state = {
films: [],
isLoading: false
}
}
_loadFilms() {
if (this.searchedText.length > 0) {
this.setState({ isLoading: true })
getFilmsFromApiWithSearchedText(this.searchedText, this.page+1).then(data => {
this.page = data.page
this.totalPages = data.total_pages
this.setState({
films: [ ...this.state.films, ...data.results ],
isLoading: false
})
})
}
}
_searchTextInputChanged(text) {
this.searchedText = text
}
_searchFilms() {
this.page = 0
this.totalPages = 0
this.setState({
films: [],
}, () => {
this._loadFilms()
})
}
render() {
return (
<View style={styles.main_container}>
<TextInput
style={styles.textinput}
placeholder='Titre du film'
onChangeText={(text) => this._searchTextInputChanged(text)}
onSubmitEditing={() => this._searchFilms()}
/>
<Button title='Rechercher' onPress={() => this._searchFilms()}/>
<FlatList
data={this.state.films}
keyExtractor={(item) => item.id.toString()}
renderItem={({item}) => <FilmItem film={item}/>}
onEndReachedThreshold={0.5}
onEndReached={() => {
if (this.page < this.totalPages) {
this._loadFilms()
}
}}
/>
</View>
)
}
}
render() {
return (
<View>
<TextInput
onChangeText={(text) => this._searchTextInputChanged(text)}
onSubmitEditing={() => this._searchFilms()}
/>
<Button title='Search' onPress={() => this._searchFilms()}/>
<FlatList
data={this.state.films}
keyExtractor={(item) => item.id.toString()}
renderItem={({item}) => <FilmItem film={item}/>}
onEndReachedThreshold={0.5}
onEndReached={() => {
if (this.page < this.totalPages) {
this._loadFilms()
}
}}
/>
{this._displayLoading()}
</View>
)
}
}
How can I translate the following with hooks :
this.page and this.totalPages ? is useRef the solution ?
in _searchFilms() I'm using setState callback to make a new API call when my film list is empty (because it's a new search). But doing it right after doesn't work because setState is asynchronous.
But I can't find a way to do it with hooks.
I think useEffect could do this but :
I only want to make this API call when my film list is empty, because I call _searchFilms() for a new search.
_loadFilms() is called on user scroll to add more films to the FlatList (for the same search) so I can't clear this.films in this case.
Here is how I translated it so far :
const Search = () => {
const [searchText, setSearchText] = useState('');
const [films, setFilms] = useState([]);
// handle pagination
const page = useRef(0);
const totalPages = useRef(0);
// handle api fetch
const [isLoading, setIsLoading] = useState(false);
const loadFilmsFromApi = () => {
getFilmsFromApiWithSearchedText(searchText, page + 1).then((data) => {
page.current = data.page;
totalPages.current = data.total_pages;
setFilms(films => [...films, ...data.results]);
setIsLoading(false);
})
};
const searchFilm = () => {
if (searchText.length > 0) {
page.current = 0;
totalPages.current = 0;
setFilms([]);
// HERE MY Films list won't be cleared (setState asynchronous)
loadFilmsFromApi();
// after the api call, clear input
setSearchText('');
}
};
useEffect(() => {
console.log(page, totalPages, "Film number" + films.length);
}, [films]);
I think you are on the right path. As for totalPages and page, having it as a ref makes sense if you want to maintain that values between different renders ( when setting state )
const Search = () => {
const [searchText, setSearchText] = useState('');
const [films, setFilms] = useState([]);
// handle pagination
const page = useRef(0);
const totalPages = useRef(0);
// handle api fetch
const [isLoading, setIsLoading] = useState(false);
// This can be invoked by either search or user scroll
// When pageNum is undefined, it means it is triggered by search
const loadFilmsFromApi = (pageNum) => {
console.log("APPEL", 'loadFills');
getFilmsFromApiWithSearchedText(searchText, pageNum ? pageNum + 1 : 1).then((data) => {
page.current = data.page;
totalPages.current = data.total_pages;
setFilms(films => {
if(pageNum) {
return [...films, ...data.results];
} else {
return [data.results];
}
});
setIsLoading(false);
})
};
useEffect(() => {
if (searchText.length > 0) {
page.current = 0;
totalPages.current = 0;
setFilms([]);
loadFilmsFromApi();
// after the api call, clear input
setSearchText('');
}
}, [searchText, loadFilmsFromApi]);
useEffect(() => {
console.log(page, totalPages, "Nombre de film " + films.length);
}, [films]);
return ( <
div > Search < /div>
);
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
Not totally clear what your question is, but it sounds like you want to clear films state before you fire off the query to the api? I am also not clear on the use of useRef here - useRef is simply a way to get a reference to an element so it's easy to access it later - like get a reference to a div and be able to access it easily via myDivRef.current
const = myDivRef = useRef;
...
<div ref={myDivRef}/>
If that is the case, then I would simply set the state of films once in the return of the API call. WRT to the refs, it seems like you this should just be normal variables, or possible state items in your function.
UPDATE:
After clearing up the goal here, you could simply add a parameter to loadFilmsFromApi to determine if you should append or overwrite:
const loadFilmsFromApi = (append) => {
getFilmsFromApiWithSearchedText(searchText, page + 1).then((data) => {
page.current = data.page;
totalPages.current = data.total_pages;
if (append) {
setFilms({
films: this.state.films.concat(data.results)
});
} else {
setFilms({
films: data.results
});
}
setIsLoading(false);
})
};