High memory usage in React Native FlatList - javascript

I am developing an app which allows the user to search for books and then display it in the search results. For displaying the results, I am using a FlatList with 3 columns and displaying the book cover and some basic information about the book.
I am storing the results from the API response in state without the comoponent. As more results are added, the memory consumption increases but the data is in JSON format, no images are store in state.
I have tried, using removeClippedSubviews and few other options that allow setting the window size but that has little to no difference on the memory usage.
Am I missing something here or is there a way to optimise this? Sample code is uploaded to this github repo
Here is the code snippet I am using:
/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* #format
* #flow strict-local
*/
import type { Node } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import {
ActivityIndicator,
FlatList,
Platform,
SafeAreaView,
StatusBar,
StyleSheet,
useColorScheme,
View,
} from 'react-native';
import { Button, SearchBar, useTheme } from 'react-native-elements';
import { searchBooks } from './api/GoogleBooksService';
import HttpClient from './network/HttpClient';
import BookCard from './components/BookCard';
const searchParamsInitialState = {
startIndex: 1,
maxResults: 12,
totalItems: null,
};
let debounceTimer;
const debounce = (callback, time) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(callback, time);
};
const isEndOfList = searchParams => {
const { startIndex, maxResults, totalItems } = searchParams;
if (totalItems == null) {
return false;
}
console.log('isEndOfList', totalItems - (startIndex - 1 + maxResults) < 0);
return totalItems - (startIndex - 1 + maxResults) < 0;
};
const App: () => Node = () => {
const isDarkMode = useColorScheme() === 'dark';
const [isLoading, setIsLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [globalSearchResults, setGlobalSearchResults] = useState([]);
const [searchParams, setSearchParams] = useState(searchParamsInitialState);
let searchCancelToken;
let searchCancelTokenSource;
// This ref will be used to track if the search Term has changed when tab is switched
const searchRef = useRef();
const clearSearch = () => {
console.log('Clear everything!');
searchRef.current = null;
setGlobalSearchResults([]);
setSearchParams(searchParamsInitialState);
setIsLoading(false);
searchCancelTokenSource?.cancel();
searchCancelToken = null;
searchCancelTokenSource = null;
};
useEffect(() => {
debounce(async () => {
setIsLoading(true);
await searchGlobal(searchTerm);
setIsLoading(false);
}, 1000);
}, [searchTerm]);
/**
* Search method
*/
const searchGlobal = async text => {
if (!text) {
// Clear everything
clearSearch();
return;
}
setIsLoading(true);
try {
// Use the initial state values if the search term has changed
let params = searchParams;
if (searchRef.current !== searchTerm) {
params = searchParamsInitialState;
}
const { items, totalItems } = await searchBooks(
text,
params.startIndex,
params.maxResults,
searchCancelTokenSource?.token,
);
if (searchRef.current === searchTerm) {
console.log('Search term has not changed. Appending data');
setGlobalSearchResults(prevState => prevState.concat(items));
setSearchParams(prevState => ({
...prevState,
startIndex: prevState.startIndex + prevState.maxResults,
totalItems,
}));
} else {
console.log(
'Search term has changed. Updating data',
searchTerm,
);
if (!searchTerm) {
console.log('!searchTerm', searchTerm);
clearSearch();
return;
}
setGlobalSearchResults(items);
setSearchParams({
...searchParamsInitialState,
startIndex:
searchParamsInitialState.startIndex +
searchParamsInitialState.maxResults,
totalItems,
});
}
searchRef.current = text;
} catch (err) {
if (HttpClient.isCancel(err)) {
console.error('Cancelled', err.message);
}
console.error(`Error searching for "${text}"`, err);
}
setIsLoading(false);
};
const renderGlobalItems = ({ item }) => {
return <BookCard book={item} />;
};
const { theme } = useTheme();
return (
<SafeAreaView style={styles.backgroundStyle}>
<StatusBar
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
/>
<View style={styles.container}>
<SearchBar
showLoading={isLoading}
placeholder="Enter search term here"
onChangeText={text => {
setSearchTerm(text);
}}
value={searchTerm}
platform={Platform.OS}
/>
{isLoading && globalSearchResults.length <= 0 && (
<ActivityIndicator animating style={styles.loader} />
)}
{globalSearchResults.length > 0 && (
<FlatList
removeClippedSubviews
columnWrapperStyle={styles.columnWrapper}
data={globalSearchResults}
numColumns={3}
showsHorizontalScrollIndicator={false}
keyExtractor={item => item + item.id}
renderItem={renderGlobalItems}
ListFooterComponent={
<>
{!isLoading &&
!isEndOfList(searchParams) &&
searchParams.totalItems > 0 && (
<Button
type="clear"
title="Load more..."
onPress={async () => {
await searchGlobal(searchTerm);
}}
/>
)}
{isLoading && searchParams.totalItems != null && (
<ActivityIndicator
size="large"
style={{
justifyContent: 'center',
}}
color={theme.colors.primary}
/>
)}
</>
}
/>
)}
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
backgroundStyle: 'white',
container: {
height: '100%',
width: '100%',
},
columnWrapper: {
flex: 1,
},
loader: {
flex: 1,
justifyContent: 'center',
},
});
export default App;

There is something called PureComponent in react native. If you create FlatList as PureComponent, you can see lot of improvement.
It will not rerender items until data has been changed.
for example:
class MyList extends React.PureComponent {
}
For more reference check this

Can you try to chuck your array of list items into small sub-arrays, this package uses this mechanism https://github.com/bolan9999/react-native-largelist
The package has been praised by complex app teams including the Discord Mobile Team - https://discord.com/blog/how-discord-achieves-native-ios-performance-with-react-native

Related

react-test-renderer test failing TypeError: Cannot read properties of undefined (reading 'current')

I'm trying to do unit tests using react-test-renderer and I can't seem to make the test pass even though the underlying component I'm testing works. Can someone tell me what I'm doing wrong
This is the code for the component
import React, { useState } from "react";
import styles from "./styles";
import { View, Pressable, Animated } from "react-native";
import Text from "components/Text/";
import { t } from "services/i18n";
import Form, { Checkbox } from "components/Form";
import WarrantyDetails from "components/WarrantyDetails";
import Icons from "icons";
import { Color } from "theme";
import animation from "./animation";
const WarrantyToggle = ({ checked = false, price = 0, onUpdate = () => { }, disabled = false }) => {
const [detailsVisible, setDetailsVisible] = useState(false);
const [isChecked, setIsChecked] = useState(checked);
const [opacity, scale] = animation(isChecked);
const onChange = (value) => {
onUpdate(value);
setIsChecked(value);
};
return (
<View style={styles.container}>
<Form
style={styles.form}
initialValues={{ warranty: checked }}>
<Checkbox
id="warranty"
name="warranty"
onChange={onChange}
disabled={disabled}
testID="boxcheck"
style={styles.checkbox}
contentStyle={styles.priceRow}
checkbox={() => (
<View style={styles.heartCheckbox}>
<Icons.Warranty size={28} color={Color.dark} strokeWidth={3} />
<Animated.View style={[styles.heart, { opacity: opacity, transform: [{ scale: scale }] }]} >
<Icons.WarrantySelected size={28} color={Color.red} strokeWidth={3} />
</Animated.View>
</View>
)}
label={() => (<Text size="3" font="bold">{t("common:add-protection-plan")}</Text>)}>
<Text style={[styles.price, { opacity: isChecked ? 1 : 0.6 }]}>{`$${price?.toFixed(2)}`}</Text>
<Pressable testID="pressdetails" size={2.5} underline onPress={() => setDetailsVisible(true)}><Text underline style={styles.details}>{t("common:details")}</Text></Pressable>
</Checkbox>
</Form>
<WarrantyDetails
testID = "warrantydetailshigher"
selected={isChecked}
setSelected={setIsChecked}
visible={detailsVisible}
onClose={() => setDetailsVisible(false)}
onChange={onChange}
price={price}
/>
</View>
);
};
export default WarrantyToggle;
This is the code for the test
import React from "react";
import renderer from "react-test-renderer";
import WarrantyToggle from "../../../src/components/WarrantyToggle";
const tree = renderer.create(<WarrantyToggle/>)
test(" Perfect lib error screen renders correctly", () => {
const tree1 = renderer
.create(
<WarrantyToggle name="hello test" />
)
.toJSON();
expect(tree1).toMatchSnapshot();
});
test('box checked' ,() =>{
const warrantycheck = tree.root.findByProps({testID:'boxcheck'})
const detailstate = tree.root.findByProps({testID:'warrantydetailshigher'})
if(detailstate.selected == true){
renderer.act(() => warrantycheck.onChange());
expect(detailstate.selected).toEqual(true)
}
else{
renderer.act(() => warrantycheck.onChange());
expect(detailstate.selected).toEqual(false)
}
});
test('press details' ,() =>{
const warrantypress = tree.root.findByProps({testID:'pressdetails'})
const detailstate = tree.root.findByProps({testID:'warrantydetailshigher'})
renderer.act(() => warrantypress.onPress());
expect(detailstate.visible).toEqual(true)
});
These are the errors I'm getting
FAIL __tests__/components/WarrantyToggle/WarrantyToggle.test.js
● Console
console.error
The above error occurred in the <WarrantyToggle> component:
at WarrantyToggle (/Users/robertbeit/Documents/sally2/app-mobile-sally-reactnative/src/components/WarrantyToggle/index.js:3777:35)
Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.
at logCapturedError (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:10989:23)
at update.callback (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:11022:5)
at callCallback (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:3662:12)
at commitUpdateQueue (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:3683:9)
at commitLifeCycles (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:11855:11)
at commitLayoutEffects (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:14443:7)
at HTMLUnknownElement.callCallback (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:11391:14)
at HTMLUnknownElement.callTheUserObjectsOperation (node_modules/jsdom/lib/jsdom/living/generated/EventListener.js:26:30)
● Perfect lib error screen renders correctly
TypeError: Cannot read properties of undefined (reading 'current')
at iconAnimation (src/components/WarrantyToggle/animation.js:411:83)
at WarrantyToggle (src/components/WarrantyToggle/index.js:3847:54)
at renderWithHooks (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:6016:18)
at mountIndeterminateComponent (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:8744:13)
at beginWork (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:9966:16)
at performUnitOfWork (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:13800:12)
at workLoopSync (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:13728:5)
at renderRootSync (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:13691:7)
at performSyncWorkOnRoot (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:13404:18)
at node_modules/react-test-renderer/cjs/react-test-renderer.development.js:2537:26
at unstable_runWithPriority (node_modules/scheduler/cjs/scheduler.development.js:468:12)
at runWithPriority (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:2486:10)
at flushSyncCallbackQueueImpl (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:2532:9)
at flushSyncCallbackQueue (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:2519:3)
at scheduleUpdateOnFiber (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:13004:9)
at updateContainer (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:16454:3)
at Object.create (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:17149:3)
at Object.<anonymous> (__tests__/components/WarrantyToggle/WarrantyToggle.test.js:8:3)
I tried running yarn test but it fails.
Edit here is the code for the animation file
import { useRef, useEffect, useCallback } from "react";
import { Animated, Easing } from "react-native";
const iconAnimation = (selected) => {
const opacityValue = useRef(new Animated.Value(0)).current;
const scaleValue = useRef(new Animated.Value(0)).current;
const animateOpacityValue = useCallback(
() => {
Animated.timing(opacityValue, {
toValue: selected ? 1 : 0,
easing: Easing.inOut(Easing.quad),
duration: 300,
useNativeDriver: true,
isInteraction: false
}).start();
Animated.timing(scaleValue, {
toValue: selected ? 1.1 : 1,
easing: selected ? Easing.out(Easing.back(25)) : Easing.inOut(Easing.quad),
duration: 500,
useNativeDriver: true,
isInteraction: false
}).start();
},
[selected],
);
useEffect(() => {
animateOpacityValue();
}, [animateOpacityValue]);
return [opacityValue, scaleValue];
};
export default iconAnimation;```
this is from the index.js form file
import React, { useRef, Fragment, useEffect } from "react";
import { View } from "react-native";
import styles from "./style";
import { Formik } from "formik";
function Row({ children, getRef = () => { }, nextElement = () => { }, values, onUpdate, onPressHandler, style }) {
children = children.filter(function (element) {
return element !== undefined;
});
return (
<View style={[styles.row, style]}>
{
React.Children.map(children, (child, index) => {
if (!child || child.props === undefined) {
return <View />;
}
const id = child.props.id;
return (
<Fragment key={`row-${index}`}>
{
React.cloneElement(child, {
containerStyle: styles.rowItem,
getRef: getRef,
value: (values && values[id]) ? values[id] : "",
values: values,
flex: child.props.flex ? child.props.flex : 1,
onUpdate: onUpdate,
onPressHandler: onPressHandler ?? child.props.onPress,
nextElement: nextElement,
})
}
{
index < children.length - 1 &&
<View style={styles.gutter} />
}
</Fragment>
);
})
}
</View>
);
}
function Form({
formRef,
children,
values,
onUpdate,
initialValues,
initialTouched,
validationSchema,
onSubmitHandler,
fieldValueRef,
resetForm = false,
style = {},
gutterStyle = {}
}) {
const internalFormRef = React.createRef(undefined);
useEffect(() => {
if ((formRef && formRef.current) && resetForm) {
formRef.current.resetForm();
} else if ((internalFormRef && internalFormRef.current) && resetForm) {
internalFormRef.current.resetForm();
}
}, [resetForm, formRef, internalFormRef]);
const inputRefs = useRef([]);
const focusNextElement = (event) => {
const index = inputRefs.current.indexOf(event.target);
if (index !== -1) {
const nextElement = inputRefs.current[index + 1];
if (nextElement) {
nextElement.focus();
}
}
};
const getRef = (element, name) => {
if (element && !inputRefs?.current?.includes(element)) {
inputRefs.current.push(element);
}
};
return (
<View style={[styles.formContainer, style]}>
<Formik
innerRef={formRef ?? internalFormRef}
initialValues={initialValues}
initialTouched={initialTouched}
enableReinitialize={true}
validateOnBlur={true}
validateOnChange
validationSchema={validationSchema}
onSubmit={onSubmitHandler}
>
{({ handleSubmit, setFieldValue }) => {
if (fieldValueRef) {
fieldValueRef.current = setFieldValue;
}
return (
<>
{
React.Children.map(children, (child, index) => {
if (!child || child.props === undefined) {
return <View />;
}
return (
<Fragment>
{
React.cloneElement(child, {
nextElement: focusNextElement,
getRef: getRef,
value: (values && values[child.props.id]) ? values[child.props.id] : "",
values: values,
onUpdate: onUpdate,
onPress: child.props.willHandleSubmit ? handleSubmit : child.props.onPress
})
}
{
index < children.length - 1 &&
<View style={[styles.gutter, gutterStyle]} />
}
</Fragment>
);
})
}
</>
);
}}
</Formik>
</View>
);
}
export default Form;
export { Row };
export { default as FormValidationSchema } from "./formValidationSchemas";
export { default as Button } from "./Button";
export { default as Link } from "./Link";
export { default as NumberInput } from "./NumberInput";
export { default as LoadingIndicator } from "./LoadingIndicator";
export { default as Checkbox } from "./Checkbox";
export { default as PLPCheckbox } from "./PLPCheckbox";
export { default as Modal } from "./Modal";
export { default as RadioGroup, RadioButton } from "./RadioGroup";
export { default as SelectInput } from "./SelectInput";
export { default as TextInput } from "./TextInput";
export { default as GooglePlacesInput } from "./GooglePlacesInput";
export { default as DatePicker } from "./DatePicker";
export { default as TextInputButton } from "./TextInputButton";
I think the problem is in animation.js file. Last error line point to animation.js file. Can you share animation.js file
iconAnimation (src/components/WarrantyToggle/animation.js:411:83)

React - generating a unique random key causes infinite loop

I have a componenet that wraps its children and slides them in and out based on the stage prop, which represents the active child's index.
As this uses a .map() to wrap each child in a div for styling, I need to give each child a key prop. I want to assign a random key as the children could be anything.
I thought I could just do this
key={`pageSlide-${uuid()}`}
but it causes an infinite loop/React to freeze and I can't figure out why
I have tried
Mapping the children before render and adding a uuid key there, calling it via key={child.uuid}
Creating an array of uuids and assigning them via key={uuids[i]}
Using a custom hook to store the children in a state and assign a uuid prop there
All result in the same issue
Currently I'm just using the child's index as a key key={pageSlide-${i}} which works but is not best practice and I want to learn why this is happening.
I can also assign the key directly to the child in the parent component and then use child.key but this kinda defeats the point of generating the key
(uuid is a function from react-uuid, but the same issue happens with any function including Math.random())
Here is the full component:
import {
Children,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import PropTypes from "prop-types";
import uuid from "react-uuid";
import ProgressBarWithTicks from "./ProgressBarWithTicks";
import { childrenPropType } from "../../../propTypes/childrenPropTypes";
const calculateTranslateX = (i = 0, stage = 0) => {
let translateX = stage === i ? 0 : 100;
if (i < stage) {
translateX = -100;
}
return translateX;
};
const ComponentSlider = ({ stage, children, stageCounter }) => {
const childComponents = Children.toArray(children);
const containerRef = useRef(null);
const [lastResize, setLastResize] = useState(null);
const [currentMaxHeight, setCurrentMaxHeight] = useState(
containerRef.current?.childNodes?.[stage]?.clientHeight
);
const updateMaxHeight = useCallback(
(scrollToTop = true) => {
if (scrollToTop) {
window.scrollTo(0, 0);
}
setCurrentMaxHeight(
Math.max(
containerRef.current?.childNodes?.[stage]?.clientHeight,
window.innerHeight -
(containerRef?.current?.offsetTop || 0) -
48
)
);
},
[stage]
);
useEffect(updateMaxHeight, [stage, updateMaxHeight]);
useEffect(() => updateMaxHeight(false), [lastResize, updateMaxHeight]);
const resizeListener = useMemo(
() => new MutationObserver(() => setLastResize(Date.now())),
[]
);
useEffect(() => {
if (containerRef.current) {
resizeListener.observe(containerRef.current, {
childList: true,
subtree: true,
});
}
}, [resizeListener]);
return (
<div className="w-100">
{stageCounter && (
<ProgressBarWithTicks
currentStage={stage}
stages={childComponents.length}
/>
)}
<div
className="position-relative divSlider align-items-start"
ref={containerRef}
style={{
maxHeight: currentMaxHeight || null,
}}>
{Children.map(childComponents, (child, i) => (
<div
key={`pageSlide-${uuid()}`}
className={`w-100 ${
stage === i ? "opacity-100" : "opacity-0"
} justify-content-center d-flex`}
style={{
zIndex: childComponents.length - i,
transform: `translateX(${calculateTranslateX(
i,
stage
)}%)`,
pointerEvents: stage === i ? null : "none",
cursor: stage === i ? null : "none",
}}>
{child}
</div>
))}
</div>
</div>
);
};
ComponentSlider.propTypes = {
children: childrenPropType.isRequired,
stage: PropTypes.number,
stageCounter: PropTypes.bool,
};
ComponentSlider.defaultProps = {
stage: 0,
stageCounter: false,
};
export default ComponentSlider;
It is only called in this component (twice, happens in both instances)
import { useEffect, useReducer, useState } from "react";
import { useParams } from "react-router-dom";
import {
FaCalendarCheck,
FaCalendarPlus,
FaHandHoldingHeart,
} from "react-icons/fa";
import { IoIosCart } from "react-icons/io";
import { mockMatches } from "../../../templates/mockData";
import { initialSwapFormState } from "../../../templates/initalStates";
import swapReducer from "../../../reducers/swapReducer";
import useFetch from "../../../hooks/useFetch";
import useValidateFields from "../../../hooks/useValidateFields";
import IconWrap from "../../common/IconWrap";
import ComponentSlider from "../../common/transitions/ComponentSlider";
import ConfirmNewSwap from "./ConfirmSwap";
import SwapFormWrapper from "./SwapFormWrapper";
import MatchSwap from "../Matches/MatchSwap";
import SwapOffers from "./SwapOffers";
import CreateNewSwap from "./CreateNewSwap";
import smallNumberToWord from "../../../functions/utils/numberToWord";
import ComponentFader from "../../common/transitions/ComponentFader";
const formStageHeaders = [
"What shift do you want to swap?",
"What shifts can you do instead?",
"Pick a matching shift",
"Good to go!",
];
const NewSwap = () => {
const { swapIdParam } = useParams();
const [formStage, setFormStage] = useState(0);
const [swapId, setSwapId] = useState(swapIdParam || null);
const [newSwap, dispatchNewSwap] = useReducer(swapReducer, {
...initialSwapFormState,
});
const [matches, setMatches] = useState(mockMatches);
const [selectedMatch, setSelectedMatch] = useState(null);
const [validateHook, newSwapValidationErrors] = useValidateFields(newSwap);
const fetchHook = useFetch();
const setStage = (stageIndex) => {
if (!swapId && stageIndex > 1) {
setSwapId(Math.round(Math.random() * 100));
}
if (stageIndex === "reset") {
setSwapId(null);
dispatchNewSwap({ type: "reset" });
}
setFormStage(stageIndex === "reset" ? 0 : stageIndex);
};
const saveMatch = async () => {
const matchResponse = await fetchHook({
type: "addSwap",
options: { body: newSwap },
});
if (matchResponse.success) {
setStage(3);
} else {
setMatches([]);
dispatchNewSwap({ type: "setSwapMatch" });
setStage(1);
}
};
useEffect(() => {
// set matchId of new selected swap
dispatchNewSwap({ type: "setSwapMatch", payload: selectedMatch });
}, [selectedMatch]);
return (
<div>
<div className="my-3">
<div className="d-flex justify-content-center w-100 my-3">
<ComponentSlider stage={formStage}>
<IconWrap colour="primary">
<FaCalendarPlus />
</IconWrap>
<IconWrap colour="danger">
<FaHandHoldingHeart />
</IconWrap>
<IconWrap colour="warning">
<IoIosCart />
</IconWrap>
<IconWrap colour="success">
<FaCalendarCheck />
</IconWrap>
</ComponentSlider>
</div>
<ComponentFader stage={formStage}>
{formStageHeaders.map((x) => (
<h3
key={`stageHeading-${x.id}`}
className="text-center my-3">
{x}
</h3>
))}
</ComponentFader>
</div>
<div className="mx-auto" style={{ maxWidth: "400px" }}>
<ComponentSlider stage={formStage} stageCounter>
<SwapFormWrapper heading="Shift details">
<CreateNewSwap
setSwapId={setSwapId}
newSwap={newSwap}
newSwapValidationErrors={newSwapValidationErrors}
dispatchNewSwap={dispatchNewSwap}
validateFunction={validateHook}
setStage={setStage}
/>
</SwapFormWrapper>
<SwapFormWrapper heading="Swap in return offers">
<p>
You can add up to{" "}
{smallNumberToWord(5).toLowerCase()} offers, and
must have at least one
</p>
<SwapOffers
swapId={swapId}
setStage={setStage}
newSwap={newSwap}
dispatchNewSwap={dispatchNewSwap}
setMatches={setMatches}
/>
</SwapFormWrapper>
<SwapFormWrapper>
<MatchSwap
swapId={swapId}
setStage={setStage}
matches={matches}
selectedMatch={selectedMatch}
setSelectedMatch={setSelectedMatch}
dispatchNewSwap={dispatchNewSwap}
saveMatch={saveMatch}
/>
</SwapFormWrapper>
<SwapFormWrapper>
<ConfirmNewSwap
swapId={swapId}
setStage={setStage}
selectedSwap={selectedMatch}
newSwap={newSwap}
/>
</SwapFormWrapper>
</ComponentSlider>
</div>
</div>
);
};
NewSwap.propTypes = {};
export default NewSwap;
One solution
#Nick Parsons has pointed out I don't even need a key if using React.Children.map(), so this is a non issue
I'd still really like to understand what was causing this problem, aas far as I can tell updateMaxHeight is involved, but I can't quite see the chain that leads to an constant re-rendering
Interstingly if I use useMemo for an array of uuids it works
const uuids = useMemo(
() => Array.from({ length: childComponents.length }).map(() => uuid()),
[childComponents.length]
);
/*...*/
key={uuids[i]}

React js google api suggestion AutocompleteService getPlacePredictions restriction to a specific state

I have the following component, which you suggest me an address.
The problem is that I need the addresses you suggest to me to refer to a single state.
Is there a way to specify this restriction via API?
Or is there a way that I could do via code in your opinion?
I thought I was doing something inside, do you want it to think it's a correct way?
Could I have problems?
React.useEffect(() => {
let active = true;
if (!autocompleteService.current && window.google)
autocompleteService.current = new window.google.maps.places.AutocompleteService();
if (!autocompleteService.current) return undefined;
if (value === '') {
setOptions([]);
return undefined;
}
fetch({ input: value }, res => {
//edit
const loc = res.map(a => (a.description.split(',').pop().trim() === 'Italia' ? a : false)).filter(Boolean);
if (active) setOptions(loc || []);
});
return () => {
active = false;
};
}, [value, fetch]);
Code:
import React from 'react';
import { useTranslation } from 'react-i18next';
import { makeStyles, TextField, Grid, Typography } from '#material-ui/core';
import { Autocomplete } from '#material-ui/lab';
import { LocationOn } from '#material-ui/icons';
import parse from 'autosuggest-highlight/parse';
import throttle from 'lodash/throttle';
function loadScript(src, position, id) {
if (!position) return;
const script = document.createElement('script');
script.setAttribute('async', '');
script.setAttribute('id', id);
script.src = src;
position.appendChild(script);
}
const autocompleteService = { current: null };
const useStyles = makeStyles(theme => ({
icon: {
color: theme.palette.text.secondary,
marginRight: theme.spacing(2)
}
}));
export default function AutocompleteGoogleMaps(props) {
const { api, value, onChange, changeinfo } = props;
const classes = useStyles();
const [options, setOptions] = React.useState([]);
const loaded = React.useRef(false);
const { i18n } = useTranslation();
let language = localStorage.getItem('lang');
if (language === null) language = i18n.language;
if (typeof window !== 'undefined' && !loaded.current) {
if (!document.querySelector('#google-maps')) {
loadScript(
`https://maps.googleapis.com/maps/api/js?key=${api}&libraries=places&language=${language}`,
document.querySelector('head'),
'google-maps'
);
}
loaded.current = true;
}
const handleChange = ({ target: { value } }) => {
onChange(value);
};
const onTagsChange = (event, val) => {
if (val !== null && val.description !== null) {
onChange({ target: { value: val.description } });
if (changeinfo) geocodeByAddress(val.description).then(changeinfo);
}
};
const fetch = React.useMemo(
() =>
throttle((input, callback) => {
autocompleteService.current.getPlacePredictions(input, callback);
}, 200),
[]
);
React.useEffect(() => {
let active = true;
if (!autocompleteService.current && window.google)
autocompleteService.current = new window.google.maps.places.AutocompleteService();
if (!autocompleteService.current) return undefined;
if (value === '') {
setOptions([]);
return undefined;
}
fetch({ input: value }, res => {
if (active) setOptions(res || []);
});
return () => {
active = false;
};
}, [value, fetch]);
const geocodeByAddress = address => {
const geocoder = new window.google.maps.Geocoder();
const { OK } = window.google.maps.GeocoderStatus;
return new Promise((resolve, reject) => {
geocoder.geocode({ address }, (results, status) => {
if (status !== OK) {
return reject(status);
}
return resolve(results);
});
});
};
return (
<Autocomplete
id="google-map"
getOptionLabel={option => (typeof option === 'string' ? option : option.description)}
filterOptions={x => x}
options={options}
autoComplete
includeInputInList
freeSolo
value={value}
renderInput={params => <TextField {...params} onChange={handleChange} {...props} />}
onChange={onTagsChange}
renderOption={({
structured_formatting: {
main_text_matched_substrings: matches,
main_text: city,
secondary_text: street
}
}) => {
const parts = parse(
city,
matches.map(match => [match.offset, match.offset + match.length])
);
return (
<Grid container alignItems="center">
<Grid item>
<LocationOn className={classes.icon} />
</Grid>
<Grid item xs>
{parts.map(({ highlight, text }, key) => (
<span key={key} style={{ fontWeight: highlight ? 700 : 400 }}>
{text}
</span>
))}
<Typography variant="body2" color="textSecondary">
{street}
</Typography>
</Grid>
</Grid>
);
}}
/>
);
}
We have a similar entry for this feature request in the Google Issue Tracker so that we can provide information on it to all Google Maps Platform APIs users, as the technical aspects of this issue are of interest to a number of our customers,
Issues and Feature Requests in the Issue Tracker are directly curated by Places API specialists, who will provide updates in the Issue Tracker whenever there's news from the engineering team.
We would like to warmly invite you to view the issue in the Issue Tracker, and to star it to register your interest. This will subscribe you to receive technical updates on the issue. Starring the issue also provides us with valuable feedback on the importance of the issue to our customers, and increases the issue's priority with the product engineering team.
You can view and star the issue here:
https://issuetracker.google.com/35822067
This Issue Tracker entry is the authoritative source for public information regarding this issue, and all publicly-relevant updates will be posted there.

API request return html markup, React - Redux

I am currently building a simple web using react with redux.
I have successfully integrated redux with app however I get 2 issue that I am unable to work out.
The first issue is that when the user apply the filter and then trigger the request the component keep rendering exceeding the limit.
The second issue is that when the user change the page the first click the request give a page markup but with the second click everything works.
These are my code so far.
action that take care of the request
import { BEGIN_FETCH_MOVIES, FETCHED_MOVIES, FETCH_FAILED_MOVIES } from '../constants';
import axios from 'axios';
//fetch movie
const searchQuery = (url) => {
return dispatch => {
//dispatch begin fetching
dispatch({
type : BEGIN_FETCH_MOVIES,
})
//make a get request to get the movies
axios.get(url)
.then((res) => {
//dispatch data if fetched
dispatch({type : FETCHED_MOVIES, payload : res.data});
})
.catch((err) => {
//dispatch error if error
dispatch({type : FETCH_FAILED_MOVIES});
});
}
//return the result after the request
}
export default searchQuery;
Main component
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { actionSearchMovie, actionSearchSerie } from '../actions'
import DisplayItemMovie from '../components/DisplayItemMovie';
import DisplayItemSerie from '../components/DisplayItemSerie';
import DrPagination from "../components/DrPagination";
import { Layout, Divider, Icon, Spin, Row } from 'antd';
//Home component
class Home extends Component {
constructor(){
super();
this.state = {
moviePage : 1,
seriePage : 1,
urlMovie : '',
urlSerie : ''
}
}
//make request before the render method is invoked
componentWillMount(){
//url
const discoverUrlMovies = 'https://api.themoviedb.org/3/discover/movie?api_key=72049b7019c79f226fad8eec6e1ee889&language=en-US&sort_by=popularity.desc&include_adult=false&include_video=false&page=1';
//requests
this.fetchMovie(discoverUrlMovies);
}
fetchMovie = ( url ) => {
this.props.actionSearchMovie(url);
}
//handle pagination
handleChangePage = (page) =>{
let url = 'https://api.themoviedb.org/3/discover/movie?api_key=72049b7019c79f226fad8eec6e1ee889&language=en-US&sort_by=popularity.desc&include_adult=false&include_video=false&page=' + page;
this.setState({
moviePage : page,
urlMovie : url
}, ()=> this.state);
this.fetchMovie(this.state.urlMovie);
}
//render
render() {
const movies = this.props.movies.results; //movies
let displayMovies; //display movies
const antIcon = <Icon type="loading" style={{ fontSize: 24 }} spin />; //spinner
//if movies and series is undefined, display a spinner
if(movies.results === undefined){
displayMovies = <Spin indicator={antIcon} />
}else {
//map through movies and series and then display the items
displayMovies = movies.results.map((movie) => {
return <DisplayItemMovie key = {movie.id} movie = {movie} />
});
}
return (
<div>
<div className='header'>
Home
</div>
<Divider />
<Layout style = {{paddingBottom : '1rem', margin : '0 auto' }}>
<h1 className = 'title'>Movie</h1>
<Row type = 'flex' style = {{flexWrap : 'wrap'}}>
{displayMovies}
</Row>
<DrPagination total = { movies.total_results } page = { this.handleChangePage } currentPage = { this.state.moviePage } /> </div>
)
}
};
const mapStateToProps = (state) => {
return{
movies : state.search_movies,
}
}
export default connect(mapStateToProps, { actionSearchMovie })(Home);
Filter component
i
mport React, { Component } from 'react';
import { Row, Col, Select, Button, Divider, InputNumber } from 'antd';
//official genres by movie db
const genres = [
{
"id": 28,
"name": "Action"
},
{
"id": 12,
"name": "Adventure"
}
];
class Filter extends Component {
constructor () {
super();
this.state = {
genre : [],
year : '',
rate : '',
filter : false,
}
}
//handle Genre
handleGenre = (genre) => {
console.log(genre);
this.setState({
genre
}, () => this.state);
}
//handle year
handleYear = (year) => {
this.setState({
year
}, () => this.state);
}
//handle Vote
handleVote = (rate) =>{
this.setState({
rate
}, ()=> this.state);
}
//handle on filter
handleFilter = () =>{
const { genre, year, rate } = this.state;
let url = 'https://api.themoviedb.org/3/discover/movie?api_key=72049b7019c79f226fad8eec6e1ee889&language=en-US&page=1';
if( genre.length === 0 && rate === "" && year === "" ){
alert('No filter is selected');
}else{
this.setState({
filter : true
});
let genreList = '';
if(genre.length > 0){
genreList = '&with_genres='+genre.join();
url += genreList;
}
if(year !== ""){
url += '&primary_release_year='+year;
}
if(rate !== ""){
url += '&vote_average='+rate;
}
//if filter === true pass the new url to the parent component
this.props.url(url);
}
}
render() {
const {filter} = this.state;
let header;
const displayGenres = genres.map((genre) => {
return (
<Select.Option key = { genre.id }>{genre.name}</Select.Option>
);
});
//
if(filter === false){
header = <h1>Top Movies</h1>
}else{
header = <h1>Custom Search</h1>
}
return (
<div>
{header}
<Divider />
<Row type = 'flex' align = 'middle' justify = 'center'>
<Col span = {14}>
<Row type = 'flex' align = 'middle' justify = 'center'>
<Col span = {5}>
<Select
mode = 'tags'
maxTagCount = {1}
style = {{width : '100%'}}
allowClear
placeholder="All Genre"
onChange = {this.handleGenre}
>
{displayGenres}
</Select>
</Col>
<Col span = {3}>
<Select
maxTagCount = {1}
style = {{width : '100%'}}
allowClear
placeholder="All Rate"
onChange = {this.handleVote}
>
<Select.Option value = '5'>>5</Select.Option>
<Select.Option value = '6'>>6</Select.Option>
<Select.Option value = '7'>>7</Select.Option>
<Select.Option value = '8'>>8</Select.Option>
<Select.Option value = '9'>>9</Select.Option>
</Select>
</Col>
<Col span = {3}>
<InputNumber
min = {1980}
max = {2019}
placeholder = 'All Year'
onChange = {this.handleYear}
/>
</Col>
</Row>
</Col>
<Col span = {5}>
<Button type = 'ghost' onClick = {this.handleFilter}>Filter</Button>
</Col>
</Row>
</div>
)
}
};
export default Filter;
Pagination component
import React, { Component } from 'react';
import { Pagination } from 'antd';
export default class DrPagination extends Component {
//on change page set the new page
handleChangePage = (page) =>{
this.props.page(page);
}
render() {
return (
<div style = {styles.container}>
<Pagination
current = {this.props.currentPage}
defaultCurrent = {1}
total = {this.props.total}
defaultPageSize = {20}
onChange = {this.handleChangePage}
size = 'small'
showQuickJumper
/>
</div>
)
}
};
const styles = {
container : {
width : '100%',
margin: '1rem',
display: 'flex',
justifyContent: 'center',
alignContent: 'center',
alignItems: 'center',
}
}
This is the data that I get when I try to change the page (it happen only for the first time).

ReactNative null is not an object (evaluating 'this.state.dataSource')

I am running the following code in Android emulator but I am getting null is not an object (evaluating 'this.state.dataSource') error.
Please, could you help me to see what I am doing wrong? For some reason the line dataSource={this.state.dataSource} is getting null.
import React, {
Component
} from 'react';
import {
AppRegistry,
ActivityIndicator,
ListView,
Text,
View,
StyleSheet
} from 'react-native';
import Row from './Row';
import Header from './Header';
import SectionHeader from './SectionHeader';
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: 20,
},
separator: {
flex: 1,
height: StyleSheet.hairlineWidth,
backgroundColor: '#8E8E8E',
},
});
export default class NoTocarList extends Component {
constructor(props) {
super(props);
const getSectionData = (dataBlob, sectionId) => dataBlob[sectionId];
const getRowData = (dataBlob, sectionId, rowId) =>
dataBlob[`${rowId}`];
fetch('http://xxxxx.mybluemix.net/get')
.then((response) => response.json())
.then((responseJson) => {
const ds = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2,
sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
getSectionData,
getRowData
});
const {
dataBlob,
sectionIds,
rowIds
} =
this.formatData(responseJson);
this.state = {
dataSource: ds.cloneWithRowsAndSections(dataBlob, sectionIds,
rowIds)
}
})
}
formatData(data) {
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
const dataBlob = {};
const sectionIds = [];
const rowIds = [];
for (let sectionId = 0; sectionId < alphabet.length; sectionId++) {
const currentChar = alphabet[sectionId];
const users = data.filter((user) =>
user.calle.toUpperCase().indexOf(currentChar) === 0);
if (users.length > 0) {
sectionIds.push(sectionId);
dataBlob[sectionId] = {
character: currentChar
};
rowIds.push([]);
for (let i = 0; i < users.length; i++) {
const rowId = `${sectionId}:${i}`;
rowIds[rowIds.length - 1].push(rowId);
dataBlob[rowId] = users[i];
}
}
}
return {
dataBlob,
sectionIds,
rowIds
};
}
render() {
return (
<View style={{flex: 1, paddingTop: 20}}>
<ListView
style={styles.container}
dataSource={this.state.dataSource}
renderRow={(rowData) => <Row {...rowData} />}
renderSeparator={(sectionId, rowId) => <View key={rowId} />}
style={styles.separator}
renderHeader={() => <Header />}
renderSectionHeader={(sectionData) => <SectionHeader {...sectionData} />}
/>
</View>
);
}
}
AppRegistry.registerComponent('NoTocar', () => NoTocarList);
From your example you have passed the dataSource as null to the ListView. So you need to initialize it first by using
this.state({
dataSource: {}
})
After getting the response from the Api call you need to set the dataSource state by using
this.setState({
dataSource: ds.cloneWithRowsAndSections(dataBlob, sectionIds,
rowIds)
})
The issue is that you're trying to update the state asynchronously, after a render, but are expecting the result on the first render. Another issue is that you're overwriting the state instead of updating it.
The fetch call in your constructor is async, meaning the constructor is finished and the component (along with its state) is created before that call resolves.
constructor() {
fetch('http://xxxxx.mybluemix.net/get')
// ...
.then(() => {
// ...
// this code gets called after the component is created
// this state is also overwriting already created state
this.state = {
dataSource: ds.cloneWithRowsAndSections(dataBlob, sectionIds,
rowIds)
}
})
}
Since your data is obtained asynchronously, you can add a check and show a progress indicator while its loading (you should also use setState instead of overwriting the state):
constructor() {
this.state = {
dataSource: null // initialize it explicitly
}
fetch('http://xxxxx.mybluemix.net/get')
// ...
.then(() => {
// ...
// use set state
this.setState({
dataSource: ds.cloneWithRowsAndSections(dataBlob, sectionIds,
rowIds)
})
})
}
render(){
// on initial render, while the state hasn't been fetched yet
// show the spinner
if (!this.state.dataSource) {
return <ActivityIndicator />
}
return(
<View style={{flex: 1, paddingTop: 20}}>
...
</View>
);
}

Categories

Resources