#javascript #reactjs #react-native #fetch #react-native-flatlist
Вопрос:
Я разрабатываю приложение на языке react-native, но я все еще новичок, поэтому я думаю, что делаю что-то не так.
Логика работы приложения заключается в следующем:
- Во — первых, я получаю список ресторанов из API
- Для каждого ресторана я показываю «карточку» со всей соответствующей информацией внутри плоского списка
- Для каждого ресторана мне нужно получить данные из второго API (URL-адрес которого зависит от идентификатора ресторана), чтобы узнать часы его работы. Затем я хочу показать ярлык «открыто» или «закрыто» для каждого ресторана
Последний шаг-это тот, который доставляет мне проблемы. Я попробовал два разных подхода, но у каждого из них есть своя проблема.
Прежде всего, часть общего:
const [data, setData] = useState([]);
useEffect(() => {
let isMounted = true;
fetch(url, {
method: 'GET',
headers:
{
'Content-Type' : 'application/json',
'Accept' : 'application/json'
}
}
)
.then((response) => {
return response.json()
})
.then((json) => {
if (isMounted)
setData(json)
})
.catch((error) => console.error(error))
.finally(() => {isMounted=false } );
}, []);
Список ресторанов сохраняется в состоянии «данные» в виде массива объектов. В каждом ресторане есть два поля «идентификатор сайта» и «источник», которые необходимы для получения часов работы из другого api.
Затем рестораны отображаются внутри плоского списка:
<FlatList
data={data}
renderItem={item => renderItem(item)}
keyExtractor={restaurant => restaurant.site_id}
/>
где renderItem выглядит следующим образом:
const renderItem = ({ item }) => (
<RestaurantCard
source={item.source}
site_id={item.site_id}
/>
)
И карточка ресторана показывает различную информацию о каждом ресторане. Чтобы упростить:
const RestaurantCard = props => (
//some other info
<View style={{ position: 'absolute', top: 0, left: 0 }}>
// THIS IS WHERE I WANT TO SHOW IF THE RESTAURANT IS OPEN OR CLOSE
</View>
So now the two approaches I used to render the opening hours info:
Approach 1
When I fetch the list of restaurant, I run over each item in the list and call a new function ‘saveOpeninghours’:
.then((json) => {
if (isMounted)
setData(json)
for (let item of json.results) {
saveOpeninghours(item.source,item.site_id)
}
})
This function then call the correspondent API for each restaurant in order to retrieve the opening hours info. This info is saved into a state array called ‘isOpen’
const [isOpen, setIsOpen] = useState([]);
const saveOpeninghours = (source,site_id) => {
let isMounted = true;
fetch(restaurantUrl, {
method: 'GET',
headers: {
'Content-Type' : 'application/json',
'Accept' : 'application/json',
}
}
)
.then((response) => {
return response.json()
})
.then((openingHours) => {
if (isMounted)
if(openingHours) {
//Do some stuff to determine if the restaurant is open or not
setIsOpen(isOpen => [...isOpen,{site_id:site_id,is_open:is_open}])
}
}
}
}
})
.catch((error) => console.error(error))
.finally(() => isMounted=false );
};
So the array will contain a list of objects in the form
{site_id:'18',is_open:true}
Then, I use another function to render this info for each restaurant:
const renderIsOpen = useCallback( (site_id,source) => {
let index = isOpen.findIndex(obj => obj.site_id === site_id)
if(index>0) {
if(isOpen[index].is_open) {
return( <Text> Open </Text>)
}
else {
return( <Text> Close </Text>)
}
}
},[isOpen])
I used a callback with [isOpen] as dependency because I want to wait until the array is filled with all restaurants.
And then in the RestaurantCard I simply call this render function:
const RestaurantCard = props => (
//some other info
<View>{renderIsOpen(props.site_id,props.source)}</View>
)
This works fine for almost all restaurants but, the problem is that the first restaurant in the list (and sometimes the second one) never shows the ‘open/close’ label. I think the reason is that the ‘isOpen’ state is not filled yet when the first restaurant is displayed, but then for the other restaurants the info is finally there and so for them there is no such problem.
I thought that using the callback with isOpen as dependency could solve the problem and force the component to re-render and show the open/close label, but it doesn’t work.
Approach 2
This is the simpler one. I created a separate component which is called for each restaurant and retrieves/renders the opening hours info:
const areEqual = (prevProps, nextProps) => true;
const OpeningHours = React.memo(props => {
const [isOpen, setIsOpen] = useState();
let isMounted = true;
fetch(restaurantUrl, {
method: 'GET',
headers: {
'Content-Type' : 'application/json',
'Accept' : 'application/json',
}
}
)
.then((response) => {
return response.json()
})
.then((openingHours) => {
if (isMounted)
if(openingHours) {
// do some stuff to determine if the restaurant is open or not
setIsOpen(is_open) //this is true or false
}
}
}
}
})
.catch((error) => console.error(error))
.finally(() => isMounted=false );
if(isOpen) {
return(
<Text> Open </Text>
)
}
else {
return(
<Text> Close </Text>
)
}
}, areEqual);
export default OpeningHours;
А затем, в моей карточке ресторана, показанной ранее, я просто вызываю этот компонент, передавая «источник» и «идентификатор сайта» в качестве реквизита:
const RestaurantCard = props => (
//some other info
<OpeningHours
source={props.source}
site_id={props.site_id}
/>
)
Проблемы с этим подходом заключаются в следующем:
- Это довольно медленно: метки открытия/закрытия отображаются на карточке ресторана с задержкой в несколько секунд. Однако это не является основной проблемой, которая заключается в следующем:
- В карточке ресторана у меня также есть кнопка, которую пользователь может нажать, чтобы добавить/удалить этот ресторан из избранного. Когда кнопка нажата, часы открытия отображаются повторно, поэтому метки открытия/закрытия появляются/исчезают, что довольно неприятно видеть. Как вы можете видеть, я попытался обернуть компонент внутри «React.memo», чтобы предотвратить повторный рендеринг, но это не работает.
Так что в основном я пытаюсь понять
- Каков наилучший подход? Я предполагаю, что это второй, так как он проще, но кажется довольно медленным по сравнению с первым (кстати, список ресторанов не такой большой, всего 10 наименований).
- Как решить проблему, связанную с выбранным подходом?
Спасибо!
Комментарии:
1. Мне, честно говоря, интересно, почему первый подход вообще «сработал»?
setIsOpen({site_id:site_id,is_open:is_open})
похоже, что вызов выборки каждого ресторана просто перезаписывает одно состояние isOpen, когда оно заканчивается. В любом случае, если вам не нравится, что ваши карты заполняются сами по себе после небольшой задержки, альтернатива состоит в том, чтобы сначала получить все данные, а только затем отобразить список. Вы можете сделать это с помощью функции Promise.all() в массиве вызовов выборки. Вы также должны определенно переработать свой код выборки в хук или просто функцию, чтобы вам не приходилось писать все этоheaders
и.json()
кодировать снова и снова.2. Правильно, на самом деле в коде, который я написал здесь, была опечатка,правильная строка была setIsOpen(isOpen => […isOpen, {идентификатор сайта:идентификатор сайта, is_open:true}]) (теперь отредактировано в исходном сообщении) Вот почему это работало. При подходе № 2 основная проблема заключается не в медлительности на самом деле, а в том факте, что метка открытия/закрытия повторно отображается каждый раз, когда пользователь нажимает кнопку «Сохранить» в ресторане
3. Если вы поднимете состояние isOpen до родительского компонента списка и передадите его в качестве реквизита, оно не изменит часы работы при повторном отображении карт.
4. Мм, но как я могу этого добиться? Проблема в том, что у меня не может быть одного состояния «Открыто» для каждого ресторана, которое можно передать компоненту (так как у меня много ресторанов).
5. Да, для этого вам нужен массив. Однако, глядя на ваш первый комментарий, обновление, которое у вас там есть, испортит заказ. Вам нужно перезаписать определенный элемент массива на основе идентификатора ресторана. Позвольте мне привести пример кода.