Unable to acess a Function with parameters inside of UseCallback - react-native

I have this page (screen) that receives via Params an ID number, in this Screen, I try to call an Action Function from my Action (reducer) file and gets an API call, I thought I didn't get any information in the Array from that call, I believe that the issue was in the Call, but I put a Console log after the declaration on the Action Function, but it didn't print so I think it didn't access to that function, so I believe the issue is in the Call of that function via Dispatch.
I even tried to put a Breakpoint inside the UseEfect where I call the Function that calls the Dispatch Function but it never breaks I'm not sure where is the error, this is the Code:
Screen (where I suspect the issue is):
```import React, {useState, useCallback, useEffect} from 'react';
import { ScrollView, Text, Image, StyleSheet, View } from 'react-native';
import { useSelector, useDispatch } from 'react-redux';
const ProductDetailScreen = props => {
const playerId = props.route.params.id;
const estadId = props.route.params.statId;
const selectedPlayer = useSelector(state => state.jugadores.availablePlayers.find(prod => prod.id === playerId));
const [isLoading, setIsLoading] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false);
const [error, setError] = useState();
const goles = useSelector(state => state.jugadores.playerGoals);
const dispatch = useDispatch();
const loadEstad = useCallback (async (param) => {
setError(null);
setIsRefreshing(true);
try {
await dispatch(userActions.fetchEstadistica(param));
} catch (err){
setError(err.message);
}
setIsRefreshing(false);
}, [dispatch, setIsLoading, setError]);
useEffect(() => {
setIsLoading(true);
loadEstad(estadId).then(() => {
setIsLoading(false);
});
}, [dispatch, loadEstad]);
console.log(estadId);
console.log(goles);
return (
<ScrollView>
<Image style={styles.image} source={{ uri: selectedPlayer.imagen }} />
<View style={styles.dataContainer}>
<Text style={styles.description}>Numero: <Text style={styles.subtitle}>{selectedPlayer.numero}</Text></Text>
<Text style={styles.description}>Nombre Completo: <Text style={styles.subtitle}>{selectedPlayer.nombre_completo}</Text></Text>
<Text style={styles.description}>Posicion: <Text style={styles.subtitle}>{selectedPlayer.posicion}</Text> </Text>
<Text style={styles.description}>Edad: <Text style={styles.subtitle}>{selectedPlayer.edad}</Text></Text>
<Text style={styles.description}>Nacionalidad: <Text style={styles.subtitle}>{selectedPlayer.nacionalidad}</Text></Text>
</View>
</ScrollView>
);
}
;
export const screenOptions = navData => {
return {
headerTitle: navData.route.params.nombre,
}
};
const
styles = StyleSheet.create({
image: {
width: '100%',
height: 300,
},
subtitle: {
fontSize: 16,
textAlign: 'justify',
marginVertical: 20,
fontWeight:'normal',
},
description: {
fontSize: 16,
textAlign: 'center',
marginVertical: 20,
fontWeight: 'bold',
},
dataContainer:{
width: '80%',
alignItems: 'center',
marginHorizontal: 40,
},
actions: {
marginVertical: 10,
alignItems: 'center',
},
});
export default ProductDetailScreen
;```
This is my Action File:
import ResultadoEstadistica from '../../models/estadistica/resultadoEstadistica';
import PlayerEstadistica from '../../models/estadistica/playerEstatisticData';
import Cards from '../../models/estadistica/cards';
import Games from '../../models/estadistica/games';
import Goals from '../../models/estadistica/goals';
export const SET_JUGADORES = 'SET_JUGADORES';
export const SET_ESTADISTICA = 'SET_ESTADISTICA';
export const fetchJugadores = () => {
return async (dispatch) => {
//any async code here!!!
try {
const response = await fetch(
'https://alianzafc2021-default-rtdb.firebaseio.com/jugadores.json'
);
if (!response.ok) {
throw new Error('Algo salio Mal!');
}
const resData = await response.json();
const loadedJugadores = [];
for (const key in resData) {
loadedJugadores.push(
new Jugador(
key,
resData[key].altura,
resData[key].apellido,
resData[key].edad,
resData[key].fecha_nacimiento,
resData[key].iso_code,
resData[key].imagen,
resData[key].lugar_nacimiento,
resData[key].nacionalidad,
resData[key].nombre_completo,
resData[key].nombre_corto,
resData[key].nombres,
resData[key].numero,
resData[key].pais,
resData[key].peso,
resData[key].player_id,
resData[key].posicion
)
);
}
dispatch({ type: SET_JUGADORES, players: loadedJugadores });
} catch (err) {
throw err;
}
};
}
export const fetchEstadistica = player_id => {
return async (dispatch) => {
//any async code here!!!
try {
const response = await fetch(
`https://api-football-v1.p.rapidapi.com/v2/players/player/${player_id}.json`,
{
method: 'GET',
headers: {
'x-rapidapi-key': Here goes my API KEY,
'x-rapidapi-host': 'api-football-v1.p.rapidapi.com',
'useQueryString': 'true'
}
}
);
if (!response.ok) {
throw new Error('Algo salio Mal!');
}
const resData = await response.json();
const loadesApiResult = [];
console.log('***Impresion desde la accion***');
console.log(resData);
console.log('***Fin de Impresionc***');
//Arrays de la Estadistica del Jugador
const loadedEstadistica = [];
const loadedCards = [];
const loadedGoals = [];
const loadedGames = [];
for (const key in resData) {
loadesApiResult.push(
new ResultadoEstadistica(
resData[key].results,
resData[key].players
)
);
}
const apiData = loadesApiResult.players;
for (const key in apiData) {
loadedEstadistica.push(
new PlayerEstadistica(
apiData[key].player_id,
apiData[key].player_name,
apiData[key].firstname,
apiData[key].lastname,
apiData[key].number,
apiData[key].position,
apiData[key].age,
apiData[key].birth_date,
apiData[key].birth_place,
apiData[key].birth_country,
apiData[key].nationality,
apiData[key].height,
apiData[key].weight,
apiData[key].injured,
apiData[key].rating,
apiData[key].team_id,
apiData[key].team_name,
apiData[key].league_id,
apiData[key].league,
apiData[key].season,
apiData[key].captain,
apiData[key].shots,
apiData[key].goals,
apiData[key].passes,
apiData[key].duels,
apiData[key].dribbles,
apiData[key].fouls,
apiData[key].cards,
apiData[key].penalty,
apiData[key].games,
apiData[key].substitutes,
)
);
}
const playerDataGames = loadedEstadistica.games;
for (const key in playerDataGames) {
loadedGames.push(
new Games(
playerDataGames[key].apperences,
playerDataGames[key].minutes_played,
playerDataGames[key].lineups
)
);
};
const playerDataGoals = loadedEstadistica.goals;
for (const key in playerDataGoals) {
loadedGoals.push(
new Goals(
playerDataGoals[key].total,
playerDataGoals[key].conceded,
playerDataGoals[key].assists,
playerDataGoals[key].saves
)
);
};
const playerDataCards = loadedEstadistica.cards;
for (const key in playerDataCards) {
loadedCards.push(
new Cards(
playerDataCards[key].yellow,
playerDataCards[key].yellowred,
playerDataCards[key].red
)
);
};
dispatch({ type: SET_ESTADISTICA, estadistica: loadesApiResult, goles: loadedGoals, juegos: loadedGames, tarjetas: loadedCards });
} catch (err) {
throw err;
}
};
};```
Finally this is my Redux Reducer just incase:
import { SET_JUGADORES, SET_ESTADISTICA } from "../actions/jugadores";
const initialState = {
availablePlayers: [],
estadistica: [],
playerGoals: [],
playerCards: [],
playerGames: [],
}
export default (state = initialState, action) => {
switch (action.type) {
case SET_JUGADORES:
return {
...state,
availablePlayers: action.players,
};
case SET_ESTADISTICA:
return{
...state,
estadistica: estadistica,
playerGoals: action.goles,
playerCards: action.tarjetas,
playerGames: action.juegos
};
}
return state;
};
Sorry for the Format but is giving me some issues; Any Ideas what my Be the Problem?
Thank you.

there are a few issues with your screen code, so i recommend simplifying the logic to make sure it works before adding anything thing else.
replace this:
const loadEstad = useCallback (async (param) => {
setError(null);
setIsRefreshing(true);
try {
await dispatch(userActions.fetchEstadistica(param));
} catch (err){
setError(err.message);
}
setIsRefreshing(false);
}, [dispatch, setIsLoading, setError]);
useEffect(() => {
setIsLoading(true);
loadEstad(estadId).then(() => {
setIsLoading(false);
});
}, [dispatch, loadEstad]);
console.log(estadId);
console.log(goles);
with this:
useEffect(()=>{
if (estadId) dispatch(userActions.fetchEstadistica(estadId));
},[estadId]);
Assuming your reducer/action code is correct, then this should call the api everytime the params estadId changes. The loading/refresh should be set in the reducer instead on the screen component.
a few things to keep in mind:
Don't await for dispatch.
console.log on state variables outside of promise resolving code block won't work.
This below won't work. instead, you should set the loading variable as a redux variable that gets updated after the API comes back with data.
loadEstad(estadId).then(() => {
setIsLoading(false);
});

Related

Reusing custom TextInput component in react native?

I am using react native 0.68.5
When I call the renderReusableTextInput method in Main.js I am sending all the styles including the needed and unneeded styles. This can lead performance penalty.
So, is there a way that I can send the only needed styles to the renderReusableTextInput method.
I also want to know is the code reusing approach of mine correct or there are better ways.
I have the following code-logic structure:
(i) A Reusable component; It has only structure no style or state
(ii) A file that includes reusable methods
(iii) The Main component, which contains components, styles and states
ReusableTextInput.js
// import ...
const ReusableTextInput = forwardRef(({
inputName,
inputLabel,
inputValue,
secureTextEntry,
textInputContainerWarperStyle,
textInputContainerStyle,
textInputLabelStyle,
textInputStyle,
textInputHelperStyle,
textInputErrorStyle,
helperText,
inputError,
onFocus,
onChangeText,
onBlur,
}, inputRef) => {
return (
<View style={[textInputContainerWarperStyle]}>
<View style={[textInputContainerStyle]}>
<Text style={[textInputLabelStyle]}>
{inputLabel}
</Text>
<TextInput
ref={(elm) => inputRef[inputName] = elm}
style={[textInputStyle]}
value={inputValue}
secureTextEntry={secureTextEntry}
onChangeText={onChangeText}
onFocus={onFocus}
onBlur={onBlur}
/>
</View>
{
((inputRef[inputName]) && (inputRef[inputName].isFocused()) && (!inputValue))
?
<Text style={[textInputHelperStyle]}>
{helperText}
</Text>
:
null
}
{
((inputError) && (inputValue))
?
<Text style={[textInputErrorStyle]}>
{inputError}
</Text>
:
null
}
</View>
);
});
export default memo(ReusableTextInput);
reusableMethods.js
// import ...
const handleFocus = (state, setState, styles) => {
const stateData = { ...state };
stateData.styleNames.textInputContainer = {
...styles.textInputContainer,
...styles[`${stateData.name}ExtraTextInputContainer`],
...styles.textInputContainerFocus,
};
stateData.styleNames.textInputLabel = {
...styles.textInputLabel,
...styles[`${stateData.name}ExtraTextInputLabel`],
...styles.textInputLabelFocus,
};
stateData.styleNames.textInput = {
...styles.textInput,
...styles[`${stateData.name}ExtraTextInput`],
...styles.textInputFocus,
};
// other logics...
setState(stateData);
};
const handleChangeText = (state, setState, text) => {
const stateData = { ...state };
// individual validation
const schemaData = Joi.object().keys(stateData.validationObj); // I used Joi for validation
const inputData = { [stateData.name]: text };
const options = { abortEarly: false, errors: { label: false } };
const result = schemaData.validate(inputData, options);
// -----
stateData.error = (result.error) ? result.error.details[0].message : '';
stateData.value = text;
// other logics...
setState(stateData);
};
const handleBlur = (state, setState, styles) => {
const stateData = { ...state };
if (stateData.value) {
stateData.styleNames.textInputContainer = {
...styles.textInputContainer,
...styles[`${stateData.name}ExtraTextInputContainer`],
...styles.textInputContainerFocus,
...styles[`${stateData.name}ExtraTextInputContainerFocus`],
...styles.textInputContainerBlurText,
...styles[`${stateData.name}ExtraTextInputContainerBlurText`],
};
stateData.styleNames.textInputLabel = {
...styles.textInputLabel,
...styles[`${stateData.name}ExtraTextInputLabel`],
...styles.textInputLabelFocus,
...styles[`${stateData.name}ExtraTextInputLabelFocus`],
...styles.textInputLabelBlurText,
...styles[`${stateData.name}ExtraTextInputLabelBlurText`],
};
stateData.styleNames.textInput = {
...styles.textInput,
...styles[`${stateData.name}ExtraTextInput`],
...styles.textInputFocus,
...styles[`${stateData.name}ExtraTextInputFocus`],
...styles.textInputBlurText,
...styles[`${stateData.name}ExtraTextInputBlurText`],
};
}
else {
stateData.styleNames.textInputContainer = { ...styles.textInputContainer, ...styles[`${stateData.name}ExtraTextInputContainer`] };
stateData.styleNames.textInputLabel = { ...styles.textInputLabel, ...styles[`${stateData.name}ExtraTextInputLabel`] };
stateData.styleNames.textInput = { ...styles.textInput, ...styles[`${stateData.name}ExtraTextInput`] };
}
// other logics...
setState(stateData);
};
// other methods...
export const renderReusableTextInput = (
state,
setState,
inputRef,
styles,
// contains all the styles from Main component and here I am sending all the styles including the needed and unneeded styles. I want improvement here
) => {
return (
<ReusableTextInput
inputName={state.name}
inputLabel={state.label}
inputValue={state.value}
inputRef={inputRef}
secureTextEntry={state.secureTextEntry}
textInputContainerWarperStyle={{...styles.textInputContainerWarper, ...styles[`${state.name}ExtraTextInputContainerWarper`]}}
textInputContainerStyle={state.styleNames.textInputContainer}
textInputLabelStyle={state.styleNames.textInputLabel}
textInputStyle={state.styleNames.textInput}
textInputHelperStyle={{...styles.textInputHelper, ...styles[`${state.name}ExtraTextInputHelper`]}}
textInputErrorStyle={{...styles.textInputError, ...styles[`${state.name}ExtraTextInputError`]}}
helperText={state.helperText}
inputError={state.error}
onFocus={() => handleFocus(state, setState, styles)}
onChangeText={(text) => handleChangeText(state, setState, text)}
onBlur={() => handleBlur(state, setState, styles)}
/>
);
};
Main.js
// import Joi from 'joi';
// import { joiPasswordExtendCore } from 'joi-password';
// import { renderReusableTextInput } from ''; ...
const schema =
{
email: Joi.string().strict()
.case("lower")
.min(5)
.max(30)
.email({ minDomainSegments: 2, tlds: { allow: ["com", "net", "org"] } })
.required(),
countryCode: // Joi.string()...,
phoneNumber: // Joi.string()...,
password: // Joi.string()...,
// ...
};
const Main = () => {
const { width: windowWidth, height: windowHeight, scale, fontScale } = useWindowDimensions();
const minimumWidth = (windowWidth <= windowHeight) ? windowWidth : windowHeight;
const styles = useMemo(() => currentStyles(minimumWidth), [minimumWidth]);
const [email, setEmail] = useState({
name: 'email', // unchangeable
label: 'Email', // unchangeable
value: '',
error: '',
validationObj: { email: schema.email }, // unchangeable
trailingIcons: [require('../../file/image/clear_trailing_icon.png')], // unchangeable
helperText: 'only .com, .net and .org allowed', // unchangeable
styleNames: {
textInputContainer: { ...styles.textInputContainer, ...styles.emailExtraTextInputContainer },
textInputLabel: { ...styles.textInputLabel, ...styles.emailExtraTextInputLabel },
textInput: { ...styles.textInput, ...styles.emailExtraTextInput },
},
});
const [phoneNumber, setPhoneNumber] = useState({
// ...
});
const [countryCode, setCountryCode] = useState({
});
const [password, setPassword] = useState({
});
// ...
const references = useRef({});
return (
<View style={[styles.mainContainer]}>
{
useMemo(() => renderReusableTextInput(email, setEmail, references.current, styles), [email, minimumWidth])
}
</View>
);
}
export default memo(Main);
const styles__575 = StyleSheet.create({
// 320 to 575
mainContainer: {
},
textInputContainerWarper: {
},
emailExtraTextInputContainerWarper: {
},
countryCodeExtraTextInputContainerWarper: {
},
phoneNumberExtraTextInputContainerWarper: {
},
passwordExtraTextInputContainerWarper: {
},
textInputContainer: {
},
emailExtraTextInputContainer: {
},
textInputContainerFocus: {
},
textInputContainerBlurText: {
},
textInputLabel: {
},
emailExtraTextInputLabel: {
},
textInputLabelFocus: {
},
textInputLabelBlurText: {
},
textInput: {
},
emailExtraTextInput: {
},
textInputFocus: {
},
textInputBlurText: {
},
textInputHelper: {
},
emailExtraTextInputHelper: {
},
textInputError: {
},
emailExtraTextInputError: {
},
// other styles...
});
const styles_576_767 = StyleSheet.create({
// 576 to 767
});
const styles_768_ = StyleSheet.create({
// 768; goes to 1024;
});
const currentStyles = (width, stylesInitial = { ...styles__575 }, styles576 = { ...styles_576_767 }, styles768 = { ...styles_768_ }) => {
let styles = {};
if (width < 576) {
// ...
}
else if ((width >= 576) && (width < 768)) {
// ...
}
else if (width >= 768) {
// ...
}
return styles;
};
I tried mentioned approach and want a better answer.
First, I think you want global styles, so that you can access it from anywhere and you can also be able to execute needed code.
Make sure you use useMemo, useCallback in right manner, for better performance.
Move all Schema and Styles and Methods of your screens inside the reusableMethods.js file (at most case). It will act like the controller of all screens of your app, also make a demo method which return a tiny component and this method execute another method, so that you can get styles for different dimentions(see below code)
Store only changeable and needed properties in state variables.
Is code reusing approach of yours, correct? I can't say about that. I will say that it depends on developer choice.
you can try like below:
reusableMethods.js
// import all schema, style variants, utility methods and other
let screenStyles = {};
const executeDimensionBasedMethods = (width, screenName) => {
if (screenName === 'a_screen_name') screenStyles[screenName] = currentStyles(width, otherParameter);
// else if()
// ...
};
export const renderDimension = (width, screenName) => {
executeDimensionBasedMethods(width, screenName);
return (
<Text style={{ width: 0, height: 0 }}></Text>
);
};
// all method definitions and logics
export const renderReusableTextInput = (
state,
setState,
inputRef,
screenName
) => {
return (
<ReusableTextInput
inputName={state.name}
inputLabel={state.label}
inputValue={state.value}
inputRef={inputRef}
secureTextEntry={state.secureTextEntry}
textInputContainerWarperStyle={{ ...screenStyles[screenName].textInputContainerWarper, ...screenStyles[screenName][`${state.name}ExtraTextInputContainerWarper`] }}
textInputContainerStyle={state.styleNames.textInputContainer || { ...screenStyles[screenName].textInputContainer }
textInputLabelStyle={state.styleNames.textInputLabel || { ...screenStyles[screenName].textInputLabel }
textInputStyle={state.styleNames.textInput || { ...screenStyles[screenName].textInput }
textInputHelperStyle={{ ...screenStyles[screenName].textInputHelper, ...screenStyles[screenName][`${state.name}ExtraTextInputHelper`] }}
textInputErrorStyle={{ ...screenStyles[screenName].textInputError, ...screenStyles[screenName][`${state.name}ExtraTextInputError`] }}
helperText={state.helperText}
inputError={state.error}
onFocus={() => handleFocus(state, setState, screenStyles[screenName])}
onChangeText={(text) => handleChangeText(state, setState, text)}
onBlur={() => handleBlur(state, setState, screenStyles[screenName])}
/>
);
};
Main.js
const Main = () => {
const { width: windowWidth, height: windowHeight} = useWindowDimensions();
const minimumWidth = (windowWidth <= windowHeight) ? windowWidth : windowHeight;
const [email, setEmail] = useState({
name: 'email', // unchangeable
label: 'Email', // unchangeable
value: '',
error: '',
trailingIcons: [require('../../file/image/clear_trailing_icon.png')], // unchangeable
helperText: 'only .com, .net and .org allowed', // unchangeable
styleNames: {
textInputContainer: undefined,
textInputLabel: undefined,
textInput: undefined,
},
});
// other states
const references = useRef({});
return (
<>
{
useMemo(() => renderDimension(minimumWidth), [minimumWidth])
}
<View style={{ // inline style}}>
{
useMemo(() => renderReusableTextInput(email, setEmail, references.current, 'nameofscreen'), [email, minimumWidth])
}
</View>
</>
}
export default memo(Main);

Geolocation clearWatch(watchId) does not stop location tracking (React Native)

I'm trying to create simple example of location tracker and I'm stuck with following case. My basic goal is to toggle location watch by pressing start/end button. I'm doing separation of concerns by implementing custom react hook which is then used in App component:
useWatchLocation.js
import {useEffect, useRef, useState} from "react"
import {PermissionsAndroid} from "react-native"
import Geolocation from "react-native-geolocation-service"
const watchCurrentLocation = async (successCallback, errorCallback) => {
if (!(await PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION))) {
errorCallback("Permissions for location are not granted!")
}
return Geolocation.watchPosition(successCallback, errorCallback, {
timeout: 3000,
maximumAge: 500,
enableHighAccuracy: true,
distanceFilter: 0,
useSignificantChanges: false,
})
}
const stopWatchingLocation = (watchId) => {
Geolocation.clearWatch(watchId)
// Geolocation.stopObserving()
}
export default useWatchLocation = () => {
const [location, setLocation] = useState()
const [lastError, setLastError] = useState()
const [locationToggle, setLocationToggle] = useState(false)
const watchId = useRef(null)
const startLocationWatch = () => {
watchId.current = watchCurrentLocation(
(position) => {
setLocation(position)
},
(error) => {
setLastError(error)
}
)
}
const cancelLocationWatch = () => {
stopWatchingLocation(watchId.current)
setLocation(null)
setLastError(null)
}
const setLocationWatch = (flag) => {
setLocationToggle(flag)
}
// execution after render when locationToggle is changed
useEffect(() => {
if (locationToggle) {
startLocationWatch()
} else cancelLocationWatch()
return cancelLocationWatch()
}, [locationToggle])
// mount / unmount
useEffect(() => {
cancelLocationWatch()
}, [])
return { location, lastError, setLocationWatch }
}
App.js
import React from "react"
import {Button, Text, View} from "react-native"
import useWatchLocation from "./hooks/useWatchLocation"
export default App = () => {
const { location, lastError, setLocationWatch } = useWatchLocation()
return (
<View style={{ margin: 20 }}>
<View style={{ margin: 20, alignItems: "center" }}>
<Text>{location && `Time: ${new Date(location.timestamp).toLocaleTimeString()}`}</Text>
<Text>{location && `Latitude: ${location.coords.latitude}`}</Text>
<Text>{location && `Longitude: ${location.coords.longitude}`}</Text>
<Text>{lastError && `Error: ${lastError}`}</Text>
</View>
<View style={{ marginTop: 20, width: "100%", flexDirection: "row", justifyContent: "space-evenly" }}>
<Button onPress={() => {setLocationWatch(true)}} title="START" />
<Button onPress={() => {setLocationWatch(false)}} title="STOP" />
</View>
</View>
)
}
I have searched multiple examples which are online and code above should work. But the problem is when stop button is pressed location still keeps getting updated even though I invoke Geolocation.clearWatch(watchId).
I wrapped Geolocation calls to handle location permission and other possible debug stuff. It seems like watchId value that is saved using useRef hook inside useWatchLocation is invalid. My guess is based on attempting to call Geolocation.stopObserving() right after Geolocation.clearWatch(watchId). Subscription stops but I get warning:
Called stopObserving with existing subscriptions.
So I assume that original subscription was not cleared.
What am I missing/doing wrong?
EDIT: I figured out solution. But since isMounted pattern is generally considered antipattern: Does anyone have a better solution?
Ok, problem solved with isMounted pattern. isMounted.current is set at locationToggle effect to true and inside cancelLocationWatch to false:
const isMounted = useRef(null)
...
useEffect(() => {
if (locationToggle) {
isMounted.current = true // <--
startLocationWatch()
} else cancelLocationWatch()
return () => cancelLocationWatch()
}, [locationToggle])
...
const cancelLocationWatch = () => {
stopWatchingLocation(watchId.current)
setLocation(null)
setLastError(null)
isMounted.current = false // <--
}
And checked at mount / unmount effect, success and error callback:
const startLocationWatch = () => {
watchId.current = watchCurrentLocation(
(position) => {
if (isMounted.current) { // <--
setLocation(position)
}
},
(error) => {
if (isMounted.current) { // <--
setLastError(error)
}
}
)
}

What is the best way to implement CRUD functionality with pagination in a FlatList?

I am currently building a very basic CRUD app in which users can seen post, delete, and update different recommendations. In the long term, it is best to use pagination rendering with lots of data just so I don't have to fetch a lot of recommendations at once.
I have an issue when I add or create a new recommendation. My app gets out of sync with my backend's pagination because it removes/adds an item from the database. What is the best way to implement the CRUD flow with a FlatList and Pagination?
Here is my recommendations reducer, actions, and FlatList:
Reducer:
import {
SET_INITIAL_RECOMMENDATIONS,
SET_NEXT_RECOMMENDATIONS,
CREATE_RECOMMENDATION,
UPDATE_RECOMMENDATION,
DELETE_RECOMMENDATION,
} from "../actions/recommendations";
const recommendations = (state = [], action) => {
switch (action.type) {
case SET_INITIAL_RECOMMENDATIONS:
return action.recommendations;
case SET_NEXT_RECOMMENDATIONS:
return [...state, ...action.nextRecommendations];
case CREATE_RECOMMENDATION:
state.pop();
return [action.recommendation, ...state];
case UPDATE_RECOMMENDATION:
const recommendationIndex = state.findIndex(
(recommendation) => recommendation.id === action.recommendationId
);
const updatedRecommendations = [...state];
updatedRecommendations[recommendationIndex] = action.recommendation;
return updatedRecommendations;
case DELETE_RECOMMENDATION:
return state.filter(
(recommendation) => recommendation.id !== action.recommendationId
);
default:
return state;
}
};
export default recommendations;
Actions:
export const fetchInitialRecommendations = () => {
return (dispatch) => {
dispatch({ type: IS_LOADING });
fetch(`${BASE_URL}/recommendations?page=1`)
.then((resp) => resp.json())
.then((recommendations) => {
dispatch({
type: SET_INITIAL_RECOMMENDATIONS,
recommendations: recommendations,
});
dispatch({ type: IS_NOT_LOADING });
})
.catch((err) => console.log(err));
};
};
export const fetchNextRecommendations = (pageNumber) => {
return (dispatch) => {
dispatch({ type: RECOMMENDATIONS_ARE_LOADING });
fetch(`${BASE_URL}/recommendations?page=${pageNumber}`)
.then((resp) => resp.json())
.then((recommendations) => {
dispatch({
type: SET_NEXT_RECOMMENDATIONS,
nextRecommendations: recommendations,
});
dispatch({ type: RECOMMENDATIONS_ARE_NOT_LOADING });
})
.catch((err) => console.log(err));
};
};
export const deleteRecommendation = (recommendationId, navigation) => {
return (dispatch, getState) => {
const userToken = getState().loggedInUser.jwt;
const reqObj = {
method: "DELETE",
headers: {
Authorization: `Berear ${userToken}`,
Accepts: "application/json",
},
};
fetch(`${BASE_URL}/recommendations/${recommendationId}`, reqObj)
.then((resp) => resp.json())
.then((data) => {
if (data.status !== 200) {
Alert.alert("Please Try Again.", data.error_messages[0], [
{ title: "OK" },
]);
} else {
dispatch({
type: DELETE_RECOMMENDATION,
recommendationId: recommendationId,
});
navigation.goBack();
}
})
.catch((err) => console.log(err));
};
};
My FlatList implementation:
import React, { Component, Fragment } from "react";
import {
View,
StyleSheet,
FlatList,
ActivityIndicator,
RefreshControl,
} from "react-native";
import { connect } from "react-redux";
import {
fetchInitialRecommendations,
refreshInitialRecommendations,
fetchNextRecommendations,
} from "../../../store/actions/recommendations";
import RecommendationCard from "../../../components/UI/browser/recommendations/RecommendationCard";
import colors from "../../../constants/colors";
class BrowserRecommendationsScreen extends Component {
constructor() {
super();
this.state = {
page: 1,
refreshing: false,
};
}
componentDidMount() {
this.props.fetchInitialRecommendations();
}
handleRefresh = () => {
this.setState({ refershing: true }, () =>
this.props.refreshInitialRecommendations()
);
this.setState({ page: 1, refreshing: false });
};
loadMoreRecommendations = () => {
this.setState(
{
page: this.state.page + 1,
},
() => this.props.fetchNextRecommendations(this.state.page)
);
};
newRecommedationOnPress = () => {
this.props.navigation.navigate("BrowserNewRecommendation");
};
render() {
if (this.props.loader) {
return (
<View style={styles.activityIndicatorScreen}>
<ActivityIndicator size="large" color={colors.primaryColor} />
</View>
);
}
return (
<View style={styles.container}>
<FlatList
contentContainerStyle={styles.flatList}
data={this.props.recommendations}
renderItem={(item) => (
<RecommendationCard
key={item.id}
id={item.id}
recommendationData={item}
navigation={this.props.navigation}
/>
)}
keyExtractor={(item) => item.id.toString()}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
onRefresh={() => this.handleRefresh()}
refreshing={this.props.recommendationsAreRefreshing}
/>
}
onEndReachedThreshold={0}
onEndReached={this.loadMoreRecommendations}
extraData={this.props.recommendations}
/>
</View>
);
}
}
const styles = StyleSheet.create({
activityIndicatorScreen: {
flex: 1,
justifyContent: "center",
alignContent: "center",
backgroundColor: colors.secondaryLight,
},
container: {
flex: 1,
width: "100%",
backgroundColor: colors.secondaryLight,
},
flatList: {
justifyContent: "center",
alignItems: "center",
width: "100%",
paddingBottom: 20,
},
});
const mapStateToProps = (state) => {
return {
loader: state.loader,
recommendationsAreLoading:
state.recommendationsLoader.recommendationsAreLoading,
recommendationsAreRefreshing:
state.recommendationsLoader.recommendationsAreRefreshing,
recommendations: state.recommendations,
};
};
const mapDispatchToProps = (dispatch) => {
return {
fetchInitialRecommendations: () => dispatch(fetchInitialRecommendations()),
refreshInitialRecommendations: () =>
dispatch(refreshInitialRecommendations()),
fetchNextRecommendations: (pageNumber) =>
dispatch(fetchNextRecommendations(pageNumber)),
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(BrowserRecommendationsScreen);
This goes back to my original question, what is the best way to implement CRUD functionality with FlatList and pagination? Or is there any work arounds? Thanks!
In my case, every time there is an action like edit, delete or add new item. I always load data from the begining (page 1) after I get response success from that action.

Invariant Violation in React Native: Text strings must be rendered within a <Text> component

I'm working on a React-Native project with REST APis, and I've currently got an invariant violation error. I've experienced this before, but I can't quite figure out what is causing it and how to fix it. If someone could point me in the right direction, I would really appreciate it! The full error is pictured below, and appears to be referencing a number of tags in the code, so I'm unsure exactly where it is originating. Thank you for reading, and thanks in advance!
The code is here:
import React, { Component } from 'react'
import { View, Text, Image, StyleSheet, FlatList} from 'react-native';
import * as Font from 'expo-font';
import styled from 'styled-components';
import dimensions from '../components/ScreenSize';
import colours from '../components/Colours';
import { Audio } from 'expo-av';
import { TouchableHighlight } from 'react-native-gesture-handler';
const client_id = {Client_ID}
const client_secret = {Client_Secret}
const item = ({item}) => (
<View style={{ flex:1, flexDirection: 'column', margin:1}}>
<TouchableHighlight onPress={() => this.fetchTracks(item.id)}>
<View>
<Text>{item.name}</Text>/>
</View>
</TouchableHighlight>
</View>
)
export default class HomeScreen extends React.Component {
state={
fontsLoaded:false,
}
async componentDidMount() {
await Font.loadAsync({
'montserrat-regular': require('../assets/fonts/Montserrat/Montserrat-Regular.ttf'),
'montserrat-light': require('../assets/fonts/Montserrat/Montserrat-Light.ttf'),
'montserrat-semibold': require('../assets/fonts/Montserrat/Montserrat-SemiBold.ttf'),
'montserrat-bold': require('../assets/fonts/Montserrat/Montserrat-Bold.ttf'),
}
).then(() => this.setState({ fontsLoaded:true }))
this.getToken();
this.setAudio();
}
constructor (props) {
super(props)
this.playbackInstance=null;
this.state = {
playing:false,
token: '',
DATA:[],
};
}
setAudio=() => {
Audio.setAudioModeAsync({
allowsRecordingIOS:false,
interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DO_NOT_MIX,
playsInSilentModeIOS: true,
shouldDuckAndroid: true,
interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DO_NOT_MIX,
playThroughEarpieceAndroid: false,
});
}
componentDidCatch(error, info)
{
console.log(error, info.componentStack);
}
getToken = async() =>
{
try
{
const getspotifytoken = await fetch("https://accounts.spotify.com/api/token",
{
method:'POST',
body: `grant_type=client_credentials&client_id=${client_id}&client_secret=${client_secret}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
const spotifytoken = await getspotifytoken.json();
this.setState({
token: spotifytoken.access_token
});
console.log(this.state.token);
}
catch(err)
{
console.log("Error fetching data", err);
}
}
search = async () => {
try
{
console.log("Searching: mood")
const spotifyApiCall = await fetch(`https://api.spotify.com/v1/browse/categories/mood/playlists?`, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${this.state.token}`,
"Content-Type":'application/json'
}
})
const spotify = await spotifyApiCall.json();
console.log("Items", spotify);
this.setState({
DATA: spotify.playlists.items,
})
}
catch (err)
{
console.log("Error fetching data", err);
}
}
fetchTracks = async (playlistId) => {
console.log('Playlist ', playlistId)
try
{
const getplaylist = await fetch(`https://api.spotify.com/v1.playlist/${playlistId}`,
{
method:'GET',
headers: {
Accept:"application/json",
Authorization:`Bearer ${this.state.token}`,
"Content-Type":"application/json"
}
});
const playlist = await getplaylist.json();
console.log('music ', playlist.tracks.items[0].preview_url);
}
catch (err)
{
console.log("Error fetching data ", err);
}
}
async _loadNewPlaybackInstance(playing, track) {
if(this.playbackInstance != null)
{
await this.playbackInstance.unloadAsync();
this.playbackInstance.setOnPlaybackStatusUpdate(null);
this.playbackInstance = null;
}
const source = {uri: track};
const initialStatus = {
shouldPlay: true,
rate: 1.0,
shouldCorrectPitch: true,
volume: 1.0,
isMuted: false
};
const {sound, status} = await Audio.Sound.createAsync(
source.initialStatus);
this.playbackInstance=sound;
this.playbackInstance.setIsLoopingAsync(false);
this.playbackInstance.playAsync();
if (this.state.selected === playlistId) {
console.log("Playing, so stop");
this.setState({selected:null});
this.playbackInstance.pauseAsync();
return;
}
this.setState({ selected:playlistId});
this._loadNewPlaybackInstance(true, playlist.tracks.items[0].preview_url);
}
render() {
if(!this.state.fontsLoaded ) {
return null
}
return (
<Container>
<Titlebar>
<Title>Music</Title>
</Titlebar>
<HeaderBar2>
<TouchableHighlight onPress={() => this.search()}>
<Header2>Playlists for your Mood</Header2>
</TouchableHighlight>
</HeaderBar2>
<View style={styles.MainContainer}>
{
this.state.DATA.length == 0 &&
<Text style={{padding:10, color:'#D3D3D3'}}/>
}
<FlatList
data = {this.state.DATA}
renderItem={item}
keyExtractor = {item.id}
numColumns={2}
extraData = {this.state}
/>
</View>
</Container>
);
}
}
I think u just have a little typo ..
check this line: <Text>{item.name}</Text>/>
change the last Text to </Text>

Read the saved data initially in react native and redux

I am learning redux. I am testing it in todo app. Everything works fine but how can I display the saved todos in the beginning. I've used AsyncStorage to store the todos list in reducer and tried to read them in mapstatetoProps function but in vain. How can I do that. Thankyou
store
import { createStore } from "redux";
import rootReducer from "../reducers";
export default (store = createStore(rootReducer));
reducer
let nextId = 0;
let newState = [];
saveTodos = newToDos => {
const saveTodos = AsyncStorage.setItem(
"savedToDoList",
JSON.stringify(newToDos)
);
};
const todos = (state = [], action) => {
switch (action.type) {
case "ADD_TODO":
newState = [
...state,
{
id: nextId++,
text: action.todo,
completed: false
}
];
this.saveTodos(newState);
return newState;
case "TOGGLE_TODO":
return state.map(
todo =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
);
default:
return state;
}
};
visibleTodo.js
const mapStateToProps = state => {
return {
todoss: state.todos
};
};
const mapDispatchToProps = dispatch => ({
toggleTodo: id => dispatch({ type: "TOGGLE_TODO", id })
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(ToDoList);
ToDoList.js
const ToDoList = ({ todoss, toggleTodo }) => (
<View style={{ padding: 20 }}>
{todoss.map(todo => (
<TouchableOpacity key={todo.id} onPress={() => toggleTodo(todo.id)}>
<Text
style={{
fontSize: 16,
color: "gray",
textDecorationLine: todo.completed ? "line-through" : "none"
}}
>
{todo.id + 1 + ". " + todo.text}
</Text>
</TouchableOpacity>
))}
</View>
);
I think that what you are looking for is redux-persist, so every time you create your store it will try to persist with async storage getting the previous data and when you add something to your store, it'll be persisted to async storage.
Here is a very good tutorial for that https://blog.reactnativecoach.com/the-definitive-guide-to-redux-persist-84738167975
Note: if you don't have nothing in your async storage yet, you'll need to add a todo first