react native : infinite loop with useEffect - javascript

When I run the app then it goes into an infinite loop
This is because pointsData is inside the useEffect.
How can this situation be fix ?
function useGetPoints() {
const [pointsData, setPointsData] = useState<PointTbleType[]>([]);
React.useEffect(() => {
loadDataFromPointTables()
}, [])
const loadDataFromPointTables = () => {
(async () => {
try {
const points = await PointTable.getPointList()
setPointsData(points);
// if (!points.length) {
// Toast.show({ type: 'info', text1: 'There is no data in the table point' })
// }
} catch (error) {
console.warn('Error from loadDataFromPointTables(): ', error);
}
})();
}
return {
pointsData
}
}
export const PointModal = (props: any) => {
const { pointsData } = useGetPoints();
const [tableHead] = useState(['Area', 'Site', 'Collection Point', 'Location']);
const [tableData, setTableData] = useState<any[]>([]);
const arrangeData = () => {
let rows: any[] = [];
pointsData.forEach(e => {
let row = [e.Area, e.Site, e.GatheringPoint, e.Location];
rows.push(row);
});
setTableData(rows);
}
useEffect(() => {
arrangeData();
}, [pointsData]);
return (
<Modal
animationType={'slide'}
transparent={false}
visible={props.pointModalVisible}
onRequestClose={() => {
console.log('Modal has been closed.');
}}>
<View style={styles.modal}>
{pointsData.length ?
<ScrollView style={styles.item}>
<View style={styles.tableView}>
<Table borderStyle={{ borderWidth: 2, borderColor: '#c8e1ff' }}>
<Row data={tableHead} style={styles.head} textStyle={styles.text} />
<Rows data={tableData} textStyle={styles.text} />
</Table>
</View>
</ScrollView>
:
<ActivityIndicator size="large" color='white' />}
</View>
<Button
title="CLOSE"
onPress={props.onClose}
/>
</Modal>
);
};

It is very obvious that it will run into an infinite loop. You're giving pointsData as dependency of useEffect and loadDataFromPointTables is dispatching value to pointsData. Remove pointsData as dependency and every will work as expected.

This is because you don't check if pointsData is not empty.
First of all I would set pointsData to null or undefined in the beginning:
const [pointsData, setPointsData] = useState<PointTbleType[]>(undefined);
and later on you have to check if it is not null or undefined and then call function to fetch data:
React.useEffect(() => {
if(!pointsData){
loadDataFromPointTables();
}
}, [pointsData])
Second option is to just remove pointsData from dependancy array in useEffect so it will run only once.

This is because you haven't set any condition while updating pointsData. Give a try with below code it should work
function useGetPoints() {
const [pointsData, setPointsData] = useState<PointTbleType[]>([]);
React.useEffect(() => {
loadDataFromPointTables()
}, [pointsData])
const loadDataFromPointTables = () => {
(async () => {
try {
const points = await PointTable.getPointList()
if(points != pointsData){
setPointsData(points);
}
} catch (error) {
console.warn('Error from loadDataFromPointTables(): ', error);
}
})();
}
return {
pointsData
}
}

Related

Function doesn't return Object but undefined instead

I want to return and object that I create in a Firestore call to my UI component.
Upon a lot of research regarding using async functions and .then() to receive data from a function, I still cannot get it to work.
I just get undefined.
Most answers I have found say that I should use await and/or .then() when handling the response so not to just get a promise. But nothing I have tried gets some actual data. The object is always undefined.
Firebase config:
export const getLatestAcceptedSample = async (bottleID, equipmentID) => {
let msg = {}
try {
await db.collection('Oil Samples').orderBy('createdAt', 'desc').limit(1).get().then(querySnapshot => {
querySnapshot.forEach(documentSnapshot => {
const tempMsg = documentSnapshot.data()
if (bottleID === tempMsg.bottleID && equipmentID === tempMsg.equipmentID) {
msg = {
bottleID: tempMsg.bottleID,
equipmentID: tempMsg.equipmentID,
timestamp: tempMsg.createdAt?.toDate() ?? '',
userName: tempMsg.authorName,
userID: tempMsg.authorID,
title: tempMsg.title
}
console.log(msg)
return msg
} else {
alert("Fetching data from database failed")
}
return msg
})
})
}
catch {
alert('Get Latest Sample error')
}
}
UI component that calls the function:
export default function SampleAcceptedScreen(props) {
const { bottleID, equipmentID } = props.route.params
const [docBottleID, setDocBottleID] = useState('')
const [docEquipmentID, setDocEquipmentID] = useState('')
const [userName, setUserName] = useState('')
useEffect(() => {
try {
FirestoreService.getLatestAcceptedSample(bottleID, equipmentID).then((msg) => {
console.log(msg)
setDocBottleID(msg.bottleID)
setDocEquipmentID(msg.equipmentID)
setUserName(msg.userName)
})
}
catch {
console.log(error)
}
})
return (
<View style={styles.container} >
<CustomHeader title="Sample Accepted" navigation={props.navigation} isHome={false} />
<View style={styles.contentContainer} >
<Text style={styles.header} >Oil sample has been registered!</Text>
<Text style={styles.header2} >The following details have been associated with the sampling:</Text>
<Text>User: {userName} </Text>
<Text>Bottle barcode: {docBottleID} </Text>
<Text>Equipment barcode: {docEquipmentID} </Text>
<TouchableOpacity style={styles.button}
onPress={() =>
props.navigation.dispatch(
CommonActions.reset({
index: 1,
routes: [
{ name: 'HomeScreen' },
{
name: 'HomeScreen',
params: { bottleID: undefined, equipmentID: undefined }
}
]
})
)} >
<Text style={styles.buttonText} >Accept</Text>
</TouchableOpacity>
</View>
</View>
);
}
I solved the issue. I moved the return msg to here:
export const getLatestAcceptedSample = async (bottleID, equipmentID) => {
let msg = {}
try {
await db.collection('Oil Samples').orderBy('createdAt', 'desc').limit(1).get().then(querySnapshot => {
querySnapshot.forEach(documentSnapshot => {
const tempMsg = documentSnapshot.data()
if (bottleID === tempMsg.bottleID && equipmentID === tempMsg.equipmentID) {
msg = {
bottleID: tempMsg.bottleID,
equipmentID: tempMsg.equipmentID,
timestamp: tempMsg.createdAt?.toDate() ?? '',
userName: tempMsg.authorName,
userID: tempMsg.authorID,
title: tempMsg.title
}
console.log(msg)
} else {
alert("Fetching data from database failed")
}
})
})
return msg
}
catch {
alert('Get Latest Sample error')
}
}
Apparently I had written the logic mostly correct but the scope for the return wasn't correct.

Prevent rerender react array of objects

I have an Ul component which contains an array of objects with different values in it. Each item called TestCase in this list has a button which makes a restcall and updates its object. However not all TestItems need to be updated. Only those whose button are clicked. The state of this array is stored in a parent continer component called TestCaseContainer. My button will updates the state accordingly to the effected TestItem in the array, however. This causes the whole list to rerender. How can I only have the changed TestItems rendered, instead of having the entire ul rendered every time an element is updated. I read about using useMemo so the component can memoize the passed down props, however I don't know how to implement this properly.
How can I stop all the rerenders?
Regression.js - Holds all the state
const Testing = forwardRef((props,ref) => {
const templateTestItem = {id:0,formData:{date:'',env:'',assetClass:'',metric:'',nodeLevel:'',nodeName:'',testName:'',dataType:'',tradeId:''},results:[],isLoading:false}
const testCaseRef = useRef()
const [isRun, setIsRun] = useState(false)
const [testItems, setTestItems] = useState([ templateTestItem])
const [stats,setStats] = useState(null)
const addTestItem = () => {
const newIndex = testItems.length
// console.log(newIndex)
const templateTestItem = {id:newIndex,formData:{date:'',env:'',assetClass:'',metric:'',nodeLevel:'',nodeName:'',testName:'',dataType:'',tradeId:''},results:[],isLoading:false}
setTestItems([...testItems, templateTestItem])
}
const addUploadCases = (cases) => {
setTestItems([])
const UploadedItems = cases.map((item,index)=>{
return{
id:index,
formData:{
date:item['date'],
env:item['env'],
assetClass:item['asset_class'],
metric:item['metric'],
nodeLevel:item['node_level'],
nodeName:item['node_name'],
testName:item['test_name'],
dataType:item['dataType'],
tradeId:item['tradeId']
},
results:[]
}
})
setTestItems(UploadedItems)
}
const runAllTests = () => {
testCaseRef.current.runAll()
}
const clearTestCases = () => {
// console.log('Clear Test cases')
setTestItems([])
if (testItems.length == 0) {
setTestItems([templateTestItem])
}
}
const extractAllResults =()=>{
testCaseRef.current.extractAllResults()
}
const updateTestResults = useCallback( (result, index) => {
console.log('Index:', index)
setTestItems(prevObjs=>(prevObjs.map((item)=>{
let updatedItem = { ...item, results: result }
if(item.id==index) return updatedItem
return item
})))
},[])
return (
<div style={{ 'backgroundColor': '#1b2829', 'display': 'flex', }} className={styles.dashboard}>
<Grid>
<Row stretched style={{}} className={styles.buttonConsole}>
{<ButtonConsole addTest={addTestItem} addUploadCases={addUploadCases} runAllTests={runAllTests} clearTestCases={clearTestCases} extractAllResults={extractAllResults} />}
</Row>
<Row centered>
<TestRunStats stats={stats}/>
</Row>
<Row style={{ 'display': 'flex', 'flex-direction': 'column' }} ><TestCaseContainer countTestRunStats={countTestRunStats} updateTestResults={updateTestResults} isRun={isRun} ref={testCaseRef} testItems={testItems} /> </Row>
{/*
<Row></Row>
<Row></Row> */}
</Grid>
</div>
);
})
TestContainer.js
const TestCaseContainer = forwardRef((props, ref) => {
const testCaseRef = useRef([])
useImperativeHandle(ref, () => ({
extractAllResults: async () => {
const data = {
data:[],
summary:[]
}
testCaseRef.current.forEach(async (item, index) => {
try {
const workbook = item.extractAllResults()
const summary = workbook['summary']
workbook['data'].forEach(testData => {
data['data'].push(testData)
})
data['summary'].push(summary)
} catch (err) {
console.log(err)
}
})
await axios.post('http://localhost:9999/api/downloadresults', data).then(res => {
console.log('res', res)
const byteCharacters = atob(res.data);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], { type: 'application/vnd.ms-excel' });
saveAs(blob, 'TestResults.xlsx')
})
},
runAll: () => {
testCaseRef.current.forEach(async (item, index) => {
await item.runAll()
})
}
}));
const runTestCase = async (date, env, nodeLevel, nodeName, assetClass, metric, dataType, tradeId, testName, key) => {
let testKey = key
console.log('FEtCHING', testKey)
try {
const params = {
nodeName,
date,
env,
nodeLevel,
assetClass,
metric,
dataType,
tradeId,
testName
}
const endpoint ={
sensitivities:'sensitivities'
}
if (metric == 'DELTA_SENSITIVITIES') {
const result = await axios.get('example.net/api/sensitivities', { params, }).then(response => {
console.log('response.data', response.data)
return response.data
})
if (result.data == 'none') {
toast.error(`${date}-${metric}-${nodeName} failed queried! No valutations for trades`, {
autoClose: 8000,
position: toast.POSITION.TOP_RIGHT
});
} else if (result.data != 'none') {
// setTestResult(result)
props.updateTestResults(result, testKey)
// updateTestResults(false,testKey,'isLoading')
toast.success(`${date}-${metric}-${nodeName} Successfully queried!`, {
autoClose: 8000,
position: toast.POSITION.TOP_RIGHT
});
}
// setTestResult(result.data)
} else {
await axios.get(`http://localhost:9999/api/metric/${metric}`, { params, }).then(response => {
if (response.data != 'none') {
props.updateTestResults(response.data, testKey)
toast.success(`${date}-${metric}-${nodeName} Successfully queried!`, {
autoClose: 8000,
position: toast.POSITION.TOP_RIGHT
});
} else {
toast.error(`${date}-${metric}-${nodeName} failed queried! No valutations for trades`, {
autoClose: 8000,
position: toast.POSITION.TOP_RIGHT
});
}
})
}
} catch (error) {
toast.error(`${date}-${metric}-${nodeName} failed queried! -${error
}`, {
autoClose: 8000,
position: toast.POSITION.TOP_RIGHT
});
}
}
return (
<Segment style={{ 'display': 'flex', 'width': 'auto', 'height': '100vh' }} className={styles.testCaseContainer}>
<div style={{ 'display': 'flex', }}>
</div>
<ul style={{overflowY:'auto',height:'100%'}} className='testItemContainer'>
{
// memoTestTwo
// testList
props.testItems.map((item, index) => {
let testName
if (item['formData']['testName'] == '') {
testName = `testRun-${index}`
} else {
testName = item['formData']['testName']
}
return <TestCase testResult={item['results']} runTestCase={runTestCase} isRun={props.isRun} ref={el => (testCaseRef.current[index] = el)} testKey={index} key={index} date={item['formData']['date']} env={item['formData']['env']} assetClass={item['formData']['assetClass']} metric={item['formData']['metric']} nodeLevel={item['formData']['nodeLevel']} nodeName={item['formData']['nodeName']} testName={testName} dataType={item['formData']['dataType']} tradeId={item['formData']['tradeId']} hierarchy={hierarchy} />
})
}
</ul>
</Segment>
)
})
TestCase.js - the individual item rendered from mapping!
const TestCase = forwardRef((props, ref) => {
const [isLoading, setIsLoading] = useState(false)
const inputRefs = useRef()
const outputRefs = useRef()
useImperativeHandle(ref, () => ({
extractAllResults: () => {
return outputRefs.current.extractAllResults();
},
runAll: () => {
inputRefs.current.runAll()
},
}));
const runSingleTestCase = async (date, env, nodeLevel, nodeName, assetClass, metric, dataType, tradeId, testName, key) => {
setIsLoading(true)
await props.runTestCase(date, env, nodeLevel, nodeName, assetClass, metric, dataType, tradeId, testName, key)
setIsLoading(false)
}
const convertDate = (date) => {
if (date) {
const newDate = date.split('/')[2] + '-' + date.split('/')[0] + '-' + date.split('/')[1]
return newDate
} else {
return date
}
}
return (
<Segment color='green' style={{ 'display': 'flex', 'flexDirection': 'column', }}>
<div style={{ 'display': 'flex', 'justify-content': 'space-between' }}>
<div style={{ 'display': 'flex', 'height': '30px' }}>
<Button
// onClick={props.deleteSingleTest(props.testKey)}
icon="close"
inverted
size="tiny"
color='red'
></Button>
</div>
<RegressionInput runSingleTestCase={runSingleTestCase} isRun={props.isRun} testKey={props.testKey} ref={inputRefs} nodeNames={props.hierarchy} runTestCase={props.runTestCase} date={convertDate(props.date)} testName={props.testName} env={props.env} assetClass={props.assetClass} metric={props.metric} nodeLevel={props.nodeLevel} nodeName={props.nodeName} dataType={props.dataType} tradeId={props.tradeId} />
<TestCheck pass={props.testResult ? props.testResult['CHECK'] : null} />
</div>
{
isLoading ? (<Loading type={'circle'} style={{ 'display': 'flex', 'flexDirecton': 'column', 'justify-content': 'center', 'align-items': 'center', 'marginTop': '50' }} inline />) : (
<RegressionOutput ref={outputRefs} testName={props.testName} testResult={props.testResult} />
)
}
</Segment>
)
})
This article might help you understand React rendering behavior better:
Blogged Answers: A (Mostly) Complete Guide to React Rendering Behavior
React's default behavior is that when a parent component renders, React will recursively render all child components inside of it!
To change that behavior you can wrap some of your components in React.memo(). So React will do a shallow compare on the props object and only re-render that if one of the top level properties of the props object has changed.
That's not always possible or recommended, especially if you are using props.children.
const TestItem = React.memo(({id,value}) => {
console.log(`Rendering TestItem ${id}...`);
return(
<div>TestItem {id}. Value: {value}</div>
);
});
const App = () => {
console.log("Rendering App...");
const [items,setItems] = React.useState([
{ id: 1, value: "INITIAL VALUE" },
{ id: 2, value: "INITIAL VALUE" },
{ id: 3, value: "INITIAL VALUE" },
]);
const testItems = items.map((item,index) =>
<TestItem key={index} id={item.id} value={item.value}/>
);
const updateTest = (index) => {
console.clear();
setItems((prevState) => {
const newArray = Array.from(prevState);
newArray[index].value = "NEW VALUE";
return newArray
});
};
return(
<React.Fragment>
<div>App</div>
<button onClick={()=>{updateTest(0)}}>Update Test 1</button>
<button onClick={()=>{updateTest(1)}}>Update Test 2</button>
<button onClick={()=>{updateTest(2)}}>Update Test 3</button>
<div>
{testItems}
</div>
</React.Fragment>
);
};
ReactDOM.render(<App/>, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"/>
Without the React.memo() call. Every re-render of the App component would trigger a re-render in all of the TestItem component that it renders.

How can I refactor code, to use one function in place of 4 that each uses a special `useState` Hook (i.e. `setAlfaIsTrue(newValue)`)

So I have the below code which I would like to refactor, so I will not have to repeat it again and again.
I made a function saveQuestion but it solves half the problem. I do not have to call
setDeleteFilters(true); setFiltersAreApplied(true); again...
But is there a way to call the saveQuestion function each time with a different hook like once with setAlfaIsTrue(newValue); then with setBetaIsTrue(newValue); etc ?
The main problem I think is the dependencies...
Thank you :)
const saveQuestion = useCallback(
() => {
setDeleteFilters(true);
setFiltersAreApplied(true);
},
[ deleteFilters, filtersAreApplied ]
);
const saveAlfa = useCallback(
(newValue) => {
setAlfaIsTrue(newValue);
saveQuestion();
// setDeleteFilters(true);
// setFiltersAreApplied(true);
},
[ alfaIsTrue ]
);
const saveBeta = useCallback(
(newValue) => {
setBetaIsTrue(newValue);
saveQuestion();
// setDeleteFilters(true);
// setFiltersAreApplied(true);
},
[ betaIsTrue ]
);
I got exactly what I wanted from T.J.Crowder
and this is how the code looks now.
Nice and concise.
const [ saveAlfa, saveBeta, saveGamma, saveDelta ] = [
setAlfaIsTrue,
setBetaIsTrue,
setGammaIsTrue,
setDeltaIsTrue
].map((fn, flag) =>
useCallback((newValue) => {
fn(newValue);
setDeleteFilters(true);
setFiltersAreApplied(true);
}, [flag])
);
thank you
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Here is all the code:
I still need to correct the dependencies...
const AnswerSwitch = (props) => {
return (
<View style={styles.multipleChoiceContainer}>
{/* <Text style={{color:Colours.moccasin_light}} >{props.label}</Text> */}
<BoldText>{props.label}</BoldText>
<View style={{ marginHorizontal: 10 }}>
<Switch
style={{ transform: [ { scaleX: 1 }, { scaleY: 0.7 } ] }}
thumbColor={Platform.OS === 'android' ? Colours.maroon : ''}
trackColor={Platform.OS === 'android' ? Colours.moccasin_light : ''}
value={props.state}
onValueChange={props.onChange}
disabled={props.disabled}
/>
</View>
</View>
);
};
////////////////////////////////////////////////////////
const DifficultyLevelSettingsScreen = (props) => {
const { width, height } = Dimensions.get('window');
const widthMultiplier = DimensionsForStyle.widthMultiplier;
const textMultiplier = DimensionsForStyle.textMultiplier;
const cardHeight = DimensionsForStyle.cardHeight;
const cardWidth = DimensionsForStyle.cardWidth;
const dispatch = useDispatch();
const [ isLoading, setIsLoading ] = useState(false);
// For the switches
// They run when a switch is touched
const [ alfaIsTrue, setAlfaIsTrue ] = useState(true);
const [ betaIsTrue, setBetaIsTrue ] = useState(true);
const [ gammaIsTrue, setGammaIsTrue ] = useState(true);
const [ deltaIsTrue, setDeltaIsTrue ] = useState(true);
// For checking if we have filters on the server so we set their state from there...
const [ filtersAreApplied, setFiltersAreApplied ] = useState(false);
// Gets updated everytime we set a filter, so we delete their previous state
const [ deleteFilters, setDeleteFilters ] = useState(false);
// Get state of filters from redux to update them
// const savedFilters = useSelector((state) => state.questions.filters);
const loadFilters = useCallback(
async () => {
const userFilters = await AsyncStorage.getItem('userFilters');
const savedFilters = JSON.parse(userFilters);
let applySavedFilters = false;
for (const key in savedFilters) {
applySavedFilters = savedFilters[key].filtersAreApplied;
// If we have savedFilters and we didn't press a switch then...
if (applySavedFilters && !deleteFilters) {
setAlfaIsTrue(savedFilters[key].alfa);
setBetaIsTrue(savedFilters[key].beta);
setGammaIsTrue(savedFilters[key].gamma);
setDeltaIsTrue(savedFilters[key].delta);
// setFiltersAreApplied(false)
}
}
// Get state of filters to send them to server
const appliedFilters = {
alfa: alfaIsTrue,
beta: betaIsTrue,
gamma: gammaIsTrue,
delta: deltaIsTrue,
filtersAreApplied: filtersAreApplied
};
// If a switch is touched then 'deleteFilters' will be true.
if (deleteFilters || savedFilters.filtersAreApplied) {
await dispatch(questionsActions.deletePreviousFilters());
await dispatch(questionsActions.setFilters(appliedFilters));
}
// Fetch them again to update the "initial" state of filters...
// dispatch(questionsActions.fetchFilters());
},
[ alfaIsTrue, betaIsTrue, gammaIsTrue, deltaIsTrue, deleteFilters, filtersAreApplied, dispatch ]
);
// loadFilters after focusing
useEffect(
() => {
const willFocusEvent = props.navigation.addListener('willFocus', loadFilters);
return () => willFocusEvent.remove();
},
[ loadFilters ]
);
// loadFilters initially...
useEffect(
() => {
setIsLoading(true);
loadFilters().then(() => setIsLoading(false));
},
[ loadFilters ]
);
// For setting the email on the HeaderTitle
useEffect(
() => {
const getData = async () => {
// Note: getItem is asynchronous, so we get a promise
await dispatch(questionsActions.fetchAllUsersData());
const userData = await AsyncStorage.getItem('userData');
if (userData) {
// parse converts a string to an object or array
const transformedData = JSON.parse(userData);
const { userEmail } = transformedData;
props.navigation.setParams({ userEmail: userEmail });
}
};
getData();
},
[ dispatch ]
);
const saveQuestion = useCallback(
() => {
// So filters on server will not be deleted
// when you open the DifficultyLevelSettingsScreen, unless you make a change.
setDeleteFilters(true);
setFiltersAreApplied(true);
},
[ deleteFilters, filtersAreApplied ]
);
// These actions run when the according switch is touched...
const saveAlfa = useCallback(
(newValue) => {
setAlfaIsTrue(newValue);
saveQuestion()
},
[ alfaIsTrue ]
);
const saveBeta = useCallback(
(newValue) => {
setBetaIsTrue(newValue);
saveQuestion()
},
[ betaIsTrue ]
);
const saveGamma = useCallback(
(newValue) => {
setGammaIsTrue(newValue);
saveQuestion()
},
[ gammaIsTrue ]
);
const saveDelta = useCallback(
(newValue) => {
setDeltaIsTrue(newValue);
saveQuestion()
},
[ deltaIsTrue ]
);
return (
// <CustomLinearGradient>
<ScrollView style={styles.screen}>
<View style={styles.difficultyLevelStyle}>
{isLoading ? (
<ActivityIndicator size="small" color={Colours.maroon} />
) : (
<BoldText style={styles.title}>Επιλογή βαθμού (ή βαθμών) δυσκολίας</BoldText>
)}
<View style={styles.switchesSummary}>
<AnswerSwitch
state={alfaIsTrue}
// onChange={(newValue) => setAlfaIsTrue(newValue)}
onChange={saveAlfa}
label="Επίπεδο: Α"
// disabled={betaIsTrue || gammaIsTrue || deltaIsTrue}
/>
<AnswerSwitch
state={betaIsTrue}
// onChange={(newValue) => setBetaIsTrue(newValue)}
onChange={saveBeta}
label="Επίπεδο: Β"
// disabled={alfaIsTrue || gammaIsTrue || deltaIsTrue}
/>
<AnswerSwitch
state={gammaIsTrue}
// onChange={(newValue) => setGammaIsTrue(newValue)}
onChange={saveGamma}
label="Επίπεδο: Γ"
// disabled={alfaIsTrue || betaIsTrue || deltaIsTrue}
/>
<AnswerSwitch
state={deltaIsTrue}
// onChange={(newValue) => setDeltaIsTrue(newValue)}
onChange={saveDelta}
label="Επίπεδο: Δ"
// disabled={alfaIsTrue || betaIsTrue || gammaIsTrue}
/>
<Line />
</View>
</View>
</ScrollView>
// </CustomLinearGradient>
);
};
DifficultyLevelSettingsScreen.navigationOptions = ({ navigation }) => {
return {
headerTitle: navigation.getParam('userEmail'),
headerLeft: (
<HeaderButtons HeaderButtonComponent={CustomHeaderButton}>
<Item
title="menu"
iconName={Platform.OS === 'android' ? 'md-menu' : 'ios-menu'}
onPress={() => navigation.toggleDrawer()}
/>
</HeaderButtons>
)
};
};
There is also an other file with 10 switches...
This custom hook will do the trick
const useSaveQuestion = (saveFunc, deps) =>
useCallback(
newValue => {
saveFunc(newValue);
setDeleteFilters(true);
setFiltersAreApplied(true);
},
[deleteFilters, filtersAreApplied, ...deps],
);
And this is how you use it:
const saveAlfa = useSaveQuestion(
newValue => {
setAlfaIsTrue(newValue);
saveQuestion();
},
[alfaIsTrue],
);
const saveBeta = useSaveQuestion(
newValue => {
setBetaIsTrue(newValue);
saveQuestion();
},
[betaIsTrue],
);

How do I pass the answer from application state as a prop and not the internal state of the card

I have a component that is giving me the following issues:
TEST STEPS:
Login to Mobile.
On the Activity Feed, answer the first Get Involved question.
Scroll to the last Get Involved question and answer it.
Scroll back to the first Get Involved question that was answered.
EXPECTED RESULTS:
The answer for the first Get Involved question should still be selected.
ACTUAL RESULTS:
It is not selected.
So it seems the issue is the callback that updates some state on the parent, but the parent is not passing the yes/no prop to this component and it's unmounting to spare me rendering.
On the backend, in the database, the answers are being recorded, but it's not persisting in the UI. I noticed that the helper functions below, specifically the this.props.onPress() return undefined.
class GetInvolvedFeedCard extends PureComponent {
static propTypes = {
onPress: PropTypes.func.isRequired,
style: PropTypes.oneOfType([PropTypes.number, PropTypes.object]),
title: PropTypes.string.isRequired
};
constructor(props) {
super(props);
const helper = `${
this.props.title.endsWith("?") ? "" : "."
} Your NFIB preferences will be updated.`;
this.state = {
noSelected: false,
yesSelected: false,
helper
};
}
_accept = () => {
this.setState({ yesSelected: true, noSelected: false }, () => {
this.props.onPress(true);
});
};
_decline = () => {
this.setState({ noSelected: true, yesSelected: false }, () => {
this.props.onPress(false);
});
};
render() {
return (
<Card style={this.props.style}>
<View style={feedContentStyles.header}>
<View style={feedContentStyles.contentType}>
<Text style={feedContentStyles.title}>{"GET INVOLVED"}</Text>
</View>
</View>
<Divider />
<View style={feedContentStyles.content}>
<View style={styles.content}>
<Text style={styles.blackTitle}>
{this.props.title}
<Text style={styles.italicText}>{this.state.helper}</Text>
</Text>
</View>
<View style={styles.footer}>
<TouchableOpacity onPress={this._decline}>
<Text
style={[
styles.btnText,
styles.noBtn,
this.state.noSelected ? styles.selected : null
]}
>
{"NO"}
</Text>
</TouchableOpacity>
<TouchableOpacity onPress={this._accept}>
<Text
style={[
styles.btnText,
this.state.yesSelected ? styles.selected : null
]}
>
{"YES"}
</Text>
</TouchableOpacity>
</View>
</View>
</Card>
);
}
}
So it appears that the state is being managed by the parent component as opposed to Redux.
What is unclear is, whether or not it is unmounting the component that defines onPress. It sounds like it is.
Just scrolling, so there is an activity feed composed of cards some of them render boolean type questions
do you want to get involved? no yes
When a user selects either response, then scrolls down a certain amount and then scrolls back up to that same question, it's like the user never answered it. I noticed when the user selects NO, the this.props.handleUpdateGetInvolved({ involved: items }) from _handleGetInvolved function is not fired only when the user selects YES. Referring to this:
_handleGetInvolved = (response, entity) => {
if (response !== entity.IsSelected) {
const isTopic = entity.Category !== "GetInvolved";
const items = [
{
...entity,
IsSelected: response
}
];
if (isTopic) {
this.props.handleUpdateTopics({ topics: items });
} else {
this.props.handleUpdateGetInvolved({ involved: items });
}
}
};
the helper functions themselves for each answer always returns undefined inside the GetInvolvedFeedCard component:
_accept = () => {
this.setState({ yesSelected: true, noSelected: false }, () => {
this.props.onPress(true);
console.log(
"this is the accept helper function: ",
this.props.onPress(true)
);
});
};
_decline = () => {
this.setState({ noSelected: true, yesSelected: false }, () => {
this.props.onPress(false);
console.log(
"this is the decline helper function: ",
this.props.onPress(false)
);
});
};
render() {
return (
<Card style={this.props.style}>
<View style={feedContentStyles.header}>
<View style={feedContentStyles.contentType}>
<Text style={feedContentStyles.title}>{"GET INVOLVED"}</Text>
</View>
</View>
<Divider />
<View style={feedContentStyles.content}>
<View style={styles.content}>
<Text style={styles.blackTitle}>
{this.props.title}
<Text style={styles.italicText}>{this.state.helper}</Text>
</Text>
</View>
<View style={styles.footer}>
<TouchableOpacity onPress={this._decline}>
<Text
style={[
styles.btnText,
styles.noBtn,
this.state.noSelected ? styles.selected : null
]}
>
{"NO"}
</Text>
</TouchableOpacity>
<TouchableOpacity onPress={this._accept}>
<Text
style={[
styles.btnText,
this.state.yesSelected ? styles.selected : null
]}
>
{"YES"}
</Text>
</TouchableOpacity>
</View>
If I am not mistaken all this is being rendered overall by the ActivityFeed component:
const { height } = Dimensions.get("window");
export class ActivityFeed extends PureComponent {
static propTypes = {
displayAlert: PropTypes.bool,
feed: PropTypes.array,
fetchFeed: PropTypes.func,
getCampaignDetails: PropTypes.func,
handleContentSwipe: PropTypes.func,
handleUpdateGetInvoved: PropTypes.func,
handleUpdateTopics: PropTypes.func,
hideUndoAlert: PropTypes.func,
lastSwippedElement: PropTypes.object,
loading: PropTypes.bool,
navigation: PropTypes.object,
setSelectedAlert: PropTypes.func,
setSelectedArticle: PropTypes.func,
setSelectedEvent: PropTypes.func,
setSelectedSurvey: PropTypes.func.isRequired,
undoSwipeAction: PropTypes.func,
userEmailIsValidForVoterVoice: PropTypes.bool
};
constructor(props) {
super(props);
this.prompted = false;
this.state = {
refreshing: false,
appState: AppState.currentState
};
}
async componentDidMount() {
AppState.addEventListener("change", this._handleAppStateChange);
if (!this.props.loading) {
const doRefresh = await cache.shouldRefresh("feed");
if (this.props.feed.length === 0 || doRefresh) {
this.props.fetchFeed();
}
cache.incrementAppViews();
}
}
componentWillUnmount() {
AppState.removeEventListener("change", this._handleAppStateChange);
}
_handleAppStateChange = async appState => {
if (
this.state.appState.match(/inactive|background/) &&
appState === "active"
) {
cache.incrementAppViews();
const doRefresh = await cache.shouldRefresh("feed");
if (doRefresh) {
this.props.fetchFeed();
}
}
this.setState({ appState });
};
_keyExtractor = ({ Entity }) =>
(Entity.Key || Entity.Id || Entity.CampaignId || Entity.Code).toString();
_gotoEvent = event => {
cache.setRouteStarter("MainDrawer");
this.props.setSelectedEvent(event);
const title = `${event.LegislatureType} Event`;
this.props.navigation.navigate("EventDetails", { title });
};
_gotoSurveyBallot = survey => {
cache.setRouteStarter("MainDrawer");
this.props.setSelectedSurvey(survey);
this.props.navigation.navigate("SurveyDetails");
};
_gotoArticle = article => {
cache.setRouteStarter("MainDrawer");
this.props.setSelectedArticle(article);
this.props.navigation.navigate("ArticleDetails");
};
_onAlertActionButtonPress = async item => {
cache.setRouteStarter("MainDrawer");
await this.props.setSelectedAlert(item.Entity);
this.props.getCampaignDetails();
if (this.props.userEmailIsValidForVoterVoice) {
this.props.navigation.navigate("Questionnaire");
} else {
this.props.navigation.navigate("UnconfirmedEmail");
}
};
_onSwipedOut = (swippedItem, index) => {
this.props.handleContentSwipe(this.props, { swippedItem, index });
};
_handleGetInvolved = (response, entity) => {
if (response !== entity.IsSelected) {
const isTopic = entity.Category !== "GetInvolved";
const items = [
{
...entity,
IsSelected: response
}
];
if (isTopic) {
this.props.handleUpdateTopics({ topics: items });
} else {
this.props.handleUpdateGetInvoved({ involved: items });
}
}
};
renderItem = ({ item, index }) => {
const { Type, Entity } = item;
if (Type === "EVENT") {
return (
<SwippableCard onSwipedOut={() => this._onSwipedOut(item, index)}>
<EventFeedCard
style={styles.push}
mainActionButtonPress={() => this._gotoEvent(Entity)}
event={Entity}
/>
</SwippableCard>
);
}
if (["SURVEY_SURVEY", "SURVEY_BALLOT"].includes(Type)) {
return (
<SwippableCard onSwipedOut={() => this._onSwipedOut(item, index)}>
<SurveyBallotFeedCard
style={styles.push}
survey={Entity}
handleViewDetails={() => this._gotoSurveyBallot(Entity)}
/>
</SwippableCard>
);
}
if (Type === "SURVEY_MICRO") {
return (
<SwippableCard onSwipedOut={() => this._onSwipedOut(item, index)}>
<MicroSurvey style={styles.push} selectedSurvey={Entity} />
</SwippableCard>
);
}
if (Type === "ALERT") {
return (
<SwippableCard onSwipedOut={() => this._onSwipedOut(item, index)}>
<ActionAlertFeedCard
datePosted={Entity.StartDateUtc}
style={styles.push}
title={Entity.Headline}
content={Entity.Alert}
mainActionButtonPress={() => this._onAlertActionButtonPress(item)}
secondaryActionButtonPress={() => {
this.props.setSelectedAlert(Entity);
// eslint-disable-next-line
this.props.navigation.navigate("ActionAlertDetails", {
content: Entity.Alert,
id: Entity.CampaignId,
title: Entity.Headline
});
}}
/>
</SwippableCard>
);
}
if (Type === "ARTICLE") {
return (
<SwippableCard onSwipedOut={() => this._onSwipedOut(item, index)}>
<ArticleFeedCard
content={Entity}
style={styles.push}
mainActionButtonPress={() => this._gotoArticle(Entity)}
/>
</SwippableCard>
);
}
//prettier-ignore
if (Type === 'NOTIFICATION' && Entity.Code === 'INDIVIDUAL_ADDRESS_HOME_MISSING') {
return (
<MissingAddressCard
style={styles.push}
navigate={() => this.props.navigation.navigate('HomeAddress')}
/>
);
}
if (["PREFERENCE_TOPIC", "PREFERENCE_INVOLVEMENT"].includes(Type)) {
return (
<SwippableCard onSwipedOut={() => this._onSwipedOut(item, index)}>
<GetInvolvedFeedCard
style={styles.push}
title={Entity.DisplayText}
onPress={response => this._handleGetInvolved(response, Entity)}
/>
</SwippableCard>
);
}
return null;
};
_onRefresh = async () => {
try {
this.setState({ refreshing: true });
this.props
.fetchFeed()
.then(() => {
this.setState({ refreshing: false });
})
.catch(() => {
this.setState({ refreshing: false });
});
} catch (e) {
this.setState({ refreshing: false });
}
};
_trackScroll = async event => {
try {
if (this.prompted) {
return;
}
const y = event.nativeEvent.contentOffset.y;
const scrollHeight = height * 0.8;
const page = Math.round(Math.floor(y) / scrollHeight);
const alert = await cache.shouldPromtpPushNotificationPermissions();
const iOS = Platform.OS === "ios";
if (alert && iOS && page > 1) {
this.prompted = true;
this._openPromptAlert();
}
} catch (e) {
return false;
}
};
_openPromptAlert = () => {
Alert.alert(
"Push Notifications Access",
"Stay engaged with NFIB on the issues and activities you care about by allowing us to notify you using push notifications",
[
{
text: "Deny",
onPress: () => {
cache.pushNotificationsPrompted();
},
style: "cancel"
},
{
text: "Allow",
onPress: () => {
OneSignal.registerForPushNotifications();
cache.pushNotificationsPrompted();
}
}
],
{ cancelable: false }
);
};
_getAlertTitle = () => {
const { lastSwippedElement } = this.props;
const { Type } = lastSwippedElement.swippedItem;
if (Type.startsWith("PREFERENCE")) {
return "Preference Dismissed";
}
switch (Type) {
case "EVENT":
return "Event Dismissed";
case "SURVEY_BALLOT":
return "Ballot Dismissed";
case "SURVEY_SURVEY":
return "Survey Dismissed";
case "SURVEY_MICRO":
return "Micro Survey Dismissed";
case "ARTICLE":
return "Article Dismissed";
case "ALERT":
return "Action Alert Dismissed";
default:
return "Dismissed";
}
};
render() {
if (this.props.loading && !this.state.refreshing) {
return <Loading />;
}
const contentStyles =
this.props.feed.length > 0 ? styles.content : emptyStateStyles.container;
return (
<View style={styles.container}>
<FlatList
contentContainerStyle={contentStyles}
showsVerticalScrollIndicator={false}
data={this.props.feed}
renderItem={this.renderItem}
keyExtractor={this._keyExtractor}
removeClippedSubviews={false}
onRefresh={this._onRefresh}
refreshing={this.state.refreshing}
ListEmptyComponent={() => (
<EmptyState navigation={this.props.navigation} />
)}
scrollEventThrottle={100}
onScroll={this._trackScroll}
/>
{this.props.displayAlert && (
<BottomAlert
title={this._getAlertTitle()}
onPress={this.props.undoSwipeAction}
hideAlert={this.props.hideUndoAlert}
/>
)}
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1
},
content: {
paddingHorizontal: scale(8),
paddingTop: scale(16),
paddingBottom: scale(20)
},
push: {
marginBottom: 16
}
});
const mapState2Props = ({
activityFeed,
auth: { userEmailIsValidForVoterVoice },
navigation
}) => {
return {
...activityFeed,
userEmailIsValidForVoterVoice,
loading: activityFeed.loading || navigation.deepLinkLoading
};
};
export default connect(mapState2Props, {
fetchFeed,
getCampaignDetails,
handleUpdateGetInvoved,
handleUpdateTopics,
setSelectedAlert,
setSelectedArticle,
setSelectedEvent,
setSelectedSurvey,
handleContentSwipe,
undoSwipeAction,
hideUndoAlert
})(ActivityFeed);
This is what the Get Involved card looks like:
So when the user clicks NO or YES, then scrolls that card away from view, the expectation is when they scroll that card back into view NO or YES, whichever one selected, should still be there.
Also, the answer selected only disappears when the user scrolls all the way to the bottom of the activity feed and then back up, but if the user only scrolls halfway through the activity feed cards and returns to this Get Involved card, the answer selected does not go away.
I believe the below SO article answer is exactly what is happening to me:
React Native - FlatList - Internal State.
So it seems the answer here would be to pass the answer from the application state as a prop and render based on that prop, and not the internal state of the card but I am not entirely sure what that looks like or how to put it together.
constructor(props) {
super(props);
// const helper = `${
// this.props.title.endsWith("?") ? "" : "."
// } Your NFIB preferences will be updated.`;
this.state = {
noSelected: false,
yesSelected: false,
helper
};
}
_accept = () => {
this.setState({ yesSelected: true, noSelected: false }, () => {
this.props.onPress(true);
});
};
_decline = () => {
this.setState({ noSelected: true, yesSelected: false }, () => {
this.props.onPress(false);
});
};
with:
this.state = {
// noSelected: false,
// yesSelected: false,
// helper
cardSelectedStatus: []
};
with the idea of then implementing cardSelectedStatus in my mapStateToProps
function mapStateToProps(state) {
return {cardSelectedStatus: state.};
}
but then I realized I am not sure what I am passing in as there are no action creators involved in this component, therefore no reducers
so can I use mapStateToProps if there is no action creator/reducer involved in this component?
utilizing mapStateToProps is the only way I know or its the way with the most experience I have in passing the user's input from the application state as a prop and render based on that prop, and not the internal state.
I can guess the problem. The problem is you are using something like FlatList. It will recreate the card every time you scroll it over screen, then scroll back.
So there are 2 ways to fix it:
Using ScrollView. It won't recreate cards
Move card states out of the Card component. Move them to List state. So in List state you will have something like:
this.state = {
cardSelectedStatus: [], // array [true, false, true, ...]. Mean card 0: selected, card 1: no selected,...
...
};
Then you can pass state to each card
So after several weeks of hammering away at this and posting the above question, here is what I finally honed in on:
By default the data for the preference is set to isSelected: false in the Redux state.
If I hit Yes, the Redux state is updated. This changes the isSelected to true.
If I hit No after hitting Yes, the Redux state is NOT updated, it stays isSelected: true.
So based on this, because isSelected does not have a neutral value, that is, starts out as false, which would equate to No, I cannot rely on Redux state for rendering the selection, which could have solved this.
Instead, in the interest of time, the only solution I could think of with much input from others includint tuledev, was to set <FlatList windowSize={500} />.
This is not an elegant solution but it will get FlatList to behave more like ScrollView in that preserve all the components so my component state will not get wiped out when a user scrolls to far away from it. This works without actually replacing it to ScrollView since there are properties and methods with FlatList that would not work by just dropping it into ScrollView same as it is in FlatList, for example, there is no renderItem property in ScrollView.

React Native how to use FlatList inside nativebase Tabs onEndReach keep fire non-stop

I'm new to react native so i use some component from 3rd party library and try to use react native component as possible.
ReactNative: 0.54
NativeBase: 2.3.10
....
i had problem with FlatList inside Tabs from Nativebase base on scrollView
onEndReachedThreshold not working correctly as Doc say 0.5 will trigger haft way scroll of item but when i set 0.5 it not trigger haft way to last item it wait until scroll to last item and it trigger onEndReach.
i had problem with onEndReach if i use ListFooterComponent to render loading when data not delivery it keep firing onEndReach non-stop.
here is my code
check props and init state
static getDerivedStateFromProps(nextProps) {
const { params } = nextProps.navigation.state;
const getCategoryId = params ? params.categoryId : 7;
const getCategoryIndex = params ? params.categoryIndex : 0;
return {
categoryId: getCategoryId,
categoryIndex: getCategoryIndex,
};
}
state = {
loadCategoryTab: { data: [] },
loadProduct: {},
storeExistId: [],
loading: false,
refreshing: false,
}
loadCategory
componentDidMount() { this.onLoadCategory(); }
onLoadCategory = () => {
axios.get(CATEGORY_API)
.then((res) => {
this.setState({ loadCategoryTab: res.data }, () => {
setTimeout(() => { this.tabIndex.goToPage(this.state.categoryIndex); });
});
}).catch(error => console.log(error));
}
Check onChange event when Tabs is swip or click
onScrollChange = () => {
const targetId = this.tabClick.props.id;
this.setState({ categoryId: targetId });
if (this.state.storeExistId.indexOf(targetId) === -1) {
this.loadProductItem(targetId);
}
}
loadProductItem = (id) => {
axios.get(`${PRODUCT_API}/${id}`)
.then((res) => {
/*
const {
current_page,
last_page,
next_page_url,
} = res.data;
*/
this.setState({
loadProduct: { ...this.state.loadProduct, [id]: res.data },
storeExistId: this.state.storeExistId.concat(id),
});
})
.catch(error => console.log(error));
}
loadMoreProduct when onEndReach is trigger
loadMoreProductItem = () => {
const { categoryId } = this.state;
const product = has.call(this.state.loadProduct, categoryId)
&& this.state.loadProduct[categoryId];
if (product.current_page !== product.last_page) {
axios.get(product.next_page_url)
.then((res) => {
const {
data,
current_page,
last_page,
next_page_url,
} = res.data;
const loadProduct = { ...this.state.loadProduct };
loadProduct[categoryId].data = product.data.concat(data);
loadProduct[categoryId].current_page = current_page;
loadProduct[categoryId].last_page = last_page;
loadProduct[categoryId].next_page_url = next_page_url;
this.setState({ loadProduct, loading: !this.state.loading });
}).catch(error => console.log(error));
} else {
this.setState({ loading: !this.state.loading });
}
}
render()
render() {
const { loadCategoryTab, loadProduct } = this.state;
const { navigation } = this.props;
return (
<Container>
<Tabs
// NB 2.3.10 not fix yet need to use `ref` to replace `initialPage`
ref={(component) => { this.tabIndex = component; }}
// initialPage={categoryIndex}
renderTabBar={() => <ScrollableTab tabsContainerStyle={styles.tabBackground} />}
onChangeTab={this.onScrollChange}
// tabBarUnderlineStyle={{ borderBottomWidth: 2 }}
>
{
loadCategoryTab.data.length > 0 &&
loadCategoryTab.data.map((parentItem) => {
const { id, name } = parentItem;
const dataItem = has.call(loadProduct, id) ? loadProduct[id].data : [];
return (
<Tab
key={id}
id={id}
ref={(tabClick) => { this.tabClick = tabClick; }}
heading={name}
tabStyle={styles.tabBackground}
activeTabStyle={styles.tabBackground}
textStyle={{ color: '#e1e4e8' }}
activeTextStyle={{ color: '#fff' }}
>
<FlatList
data={dataItem}
keyExtractor={subItem => String(subItem.prod_id)}
ListEmptyComponent={this.onFirstLoad}
// ListFooterComponent={this.onFooterLoad}
refreshing={this.state.refreshing}
onRefresh={this.handleRefresh}
onEndReachedThreshold={0.5}
onEndReached={() => {
this.setState({ loading: !this.state.loading }, this.loadMoreProductItem);
}}
renderItem={({ item }) => {
const productItems = {
item,
navigation,
};
return (
<ProductItems {...productItems} />
);
}}
/>
// this OnLoadFooter is my tempory show loading without ListFooterComponent but i don't want to show loading outside FlatList hope i will get a help soon
<OnLoadFooter loading={this.state.loading} style={{ backgroundColor: '#fff' }} />
</Tab>
);
})
}
</Tabs>
</Container>
);
}
Loading Component
function OnLoadFooter(props) {
if (props.loading) return <Spinner style={{ height: 50, paddingVertical: 10 }} />;
return null;
}
Let me explain my process
init CategoryId and CategoIndex for Tabs active
after axios fire will get all category and render Tab item because nativebase Tabs bug when initailPage bigger than 0 it show blank page and i use ref trigger it when category complete load when this.tabIndex.goToPage is trigger it call onChange
onChage event start to check if tabClick Ref exist in StoreExistId that save category when they click if true we load product else we do nothing. i need ref in this because React state is async making my product fire loading duplicate data for 1st time so Ref come in to fix this.
when scroll down to last item it will loadMoreProduct by paginate on API
my data in state like below
StoreExistId: [1,2,3,4]
loadProduct: {
1: {data: [.....]},
2: {data: [.....]},
etc....
}
Thank in advanced
Some of NativeBase components use scrollView inside. I guess it could be ScrollableTab component which uses ScrollView? You should not use FlatList inside ScrollView, onReachEnd will not work then.
I was facing the same problem, solution is to use <FlatList> inside <Content> . For more information see https://stackoverflow.com/a/54305517/8858217

Categories

Resources