I am trying to render a list of exercises using React Native's FlatList component, and I'm currently unable to successfully edit the name of the exercise. Everything runs smoothly upon the initial render. However, when I try to edit the exercise, the subsequent re-render returns the error undefined is not an object (evaluating 'item.id'). If I check my local database, the data is successfully edited, but rendering it to the screen is another issue. If I were to dismiss the error display and reload the app, however, the expected changes have been made to the exercise.
I've seen previous posts/responses surrounding this issue, but from what I can tell, I've tried the suggested solutions to no avail.
Here is the component responsible for rendering the FlatList:
function Exercises({ fetchExercises }) {
const dispatch = useDispatch()
const exercises = useSelector(state => state.exercises) // **passed to data prop**
useEffect(() => {
dispatch(fetchExercises())
}, [dispatch])
<FlatList
data={exercises}
keyExtractor={item => item.id.toString()}
renderItem={({ item }) => {
return <ExerciseListItem item={item} />
}}
/>
Below is the action I'm dispatching in the useEffect:
export const editExercise = (id, changes) => dispatch => {
dispatch({ type: EXERCISE_START })
axios.put(<endpoint>, { name: changes })
.then(res => {
dispatch({ type: EDIT_EXERCISE_SUCCESS, payload: res.data })
.catch(err => {
dispatch({ type: EDIT_EXERCISE_FAIL, payload: err })
}
}
...and here is my reducer:
const initialState = {
exercises: [],
}
const exercisesReducer = (state = initialState, action) => {
switch (action.type) {
case EDIT_EXERCISE:
return {
...state,
isLoading: false,
exercises: state.exercises.map(exercise=> {
exercise.id === action.payload.id ? action.payload : exercise
}
}
}
}
For what it's worth, I'm able to successfully add a new exercise to the list. Not sure if that would make a difference, but it's worth mentioning. Also, here is a similar question that someone asked about a year ago with what would appear to be the solution to my issue, but I'm still not getting the expected results.
Any help would be greatly appreciated!
I think the issue is that you're not returning anything in your map which is leading to this error.
exercises: state.exercises.map(exercise=> {
return exercise.id === action.payload.id ? action.payload : exercise
}
Related
I have two screens. Approve List and Approve Detail. When data approved in Approve Detail, page navigate to Approve List. Then approved data should disapear from FLatList. How to remove FlatList item when data approved? or how to re render FlatList when data change? Here is my code:
Approve List:
const Approve = ({ navigation }) => {
const [rekomendasi, setRekomendasi] = useState({})
// other code
const getRekomendasi = async (token, bagian) => {
try {
const response = await sippApi.get(`/penjaminan?bagian=${bagian}`, {
headers: {
Auth: token
}
});
setRekomendasi(response.data.data)
console.log(rekomendasi)
} catch (error) {
console.log(error)
}
}
useEffect(() => {
getToken();
getUserData()
getRekomendasi(token, userData.bagian);
}, [setToken, setUserData, rekomendasi]); // if I pass rekomendasi here, make infinite loop on api request
return (
<FlatList
onRefresh={() => onRefresh()}
refreshing={isFetching}
removeClippedSubviews
style={{ marginTop: 2 }}
data={rekomendasi}
keyExtractor={rekom => rekom.penjaminan.nomor_rekomendasi}
renderItem={({ item }) => {
return (
<TouchableOpacity onPress={() => navigation.navigate("ApproveDetail", { id: item.penjaminan.nomor_rekomendasi, bagian: userData.bagian })}>
<ApproveList
plafond={item.value}
kantor={item.nama_kantor}
nomor_rekomendasi={item.nomor_rekomendasi}
produk={item.skim}
/>
</TouchableOpacity>
)
}}
showsHorizontalScrollIndicator={false}
/>
)
}
If I pass value on second argument on UseEffect, it cause infinite loop on API request. If not, my FlatList cant re render when data change. What should I do?
Thanks for help
You have to remove the rekomendasi dependency in the useEffect to avoid infinite loop, it's only for init data :)
What is the purpose of onRefresh function in the FlatList ? Instead you could put the getRekomendasi function to trigger a new call and your data will be updated
try to separate the functions to two useEffects
useEffect(() => {
//<-- write your getToken() and getUserDate() here
getToken();
getUserData()
}, []);
useEffect(() => {
const getRekomendasi = async (token, bagian) => {
try {
const response = await sippApi.get(`/penjaminan?bagian=${bagian}`, {
headers: {
Auth: token
}
});
setRekomendasi(response.data.data)
console.log(rekomendasi)
} catch (error) {
console.log(error)
}
}
getRekomendasi(token, userData.bagian);
},[token,userData.bagian]);
Problem solved by using useFocusEffect
useFocusEffect(
React.useCallback(() => {
getRekomendasi(token, userData.bagian)
}, [token, userData.bagian])
);
I am getting a very annoying Maximum call stack size exceeded. when loading data using <FlatList> and onEndReached. The onRefresh does not cause this. I presume this has to do with the <FeedList> re-rendering when I update the data.
Can someone help me fix this or at least point me in the right direction?
export function useFeedState() {
const state = useState(feedState);
const api = useApi();
function loadPosts(clear = false) {
api
.request<{ posts: IPost[] }>('get', 'posts', { feed: state.feed.value })
.then((data) => {
state.posts.set(
clear ? data.posts : [...state.posts.value, ...data.posts]
);
})
.catch((error) => {
console.log(error);
});
}
// ...
}
Component:
export default function FeedList() {
const feedState = useFeedState();
return (
<FlatList
data={feedState.posts}
renderItem={({ item }) => <PostSlide post={item} />}
keyExtractor={(item) => item.id.toString()}
refreshing={feedState.isLoading}
onRefresh={() => feedState.loadPosts(true)}
onEndReached={() => {
feedState.loadPosts();
}}
/>
);
}
I solved this by using a callback in the set-method. Hookstate is built upon Reacts state and therefor has some similarities. You can read more about when to use a callback here.
// works
state.posts.set((prev) => {
return clear ? data.posts : [...prev, ...data.posts];
});
// also works
clear ? state.posts.set(data.posts) : state.posts.merge(data.posts);
// doesn't work
state.posts.set(
clear ? data.posts : [...state.posts.value, ...data.posts]
);
I guess maybe the reason for the loop was caused by using the value of the same state I was updating.
I have set up a store function
export const storeData = async text => {
try {
await AsyncStorage.getItem("notes")
.then((notes) => {
const noteList = notes ? JSON.parse(notes) : [];
noteList.push(text);
AsyncStorage.setItem('notes', JSON.stringify(noteList));
});
} catch (error) {
console.log("error saving" + error);
}
};
When calling from the header back button it works as intended
navigation.setOptions({
headerLeft: () => (
<HeaderBackButton onPress={() => {
storeData(text).then(() => {
navigation.goBack();
}
}} />
)
});
But when using it from the hardware back button it gives me an "unhandled promise rejection, undefined is not an object. evaluating _this.navigation".
useEffect(() => {
const backHandler = BackHandler.addEventListener("hardwareBackPress", () => {
storeData(text).then(() => {
this.navigation.goBack();
});
});
return () => backHandler.remove();
}, [text]);
Can anyone see what might cause this behaviour?
replace this by props. thiskey word is used mainly in class components here i its a functional components so navigation is reached by props.navigation
The full code would look like
function EditNoteScreen({ navigation }) {
const [text, setText] = useState("");
const backAction = () => {
storeData(text).then(() => {
Keyboard.dismiss();
navigation.goBack();
});
}
useEffect(() => {
const backHandler = BackHandler.addEventListener("hardwareBackPress", () => {
backAction();
});
navigation.setOptions({
headerLeft: () => (
<HeaderBackButton onPress={() => {
backAction();
}} />
)
});
return () => backHandler.remove();
}, [text]);
If I simply have my storage function run with the hardware back press the code will work and the hardware back buttons default behavior will take me back, but then the new item will not show up until refreshed, which is why i want the back behavior delayed until saving is done.
One way to ignore this would simply be to update the flatlist again on state change, but I would rather have the information there from the refresh rather then popping in.
Hey I am new to React Native and currently I'm trying to put data in a picker using data from API. I'm confused that it got error say TypeError: null is not an object (evaluating this.state.schedules.map). Is there something wrong with the state or is there any concept that I misunderstood
Here is fetch API
export function getSchedule (token, resultCB) {
var endpoint = "/api/getList"
let header = {
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": "Bearer " + token
};
return dispatch => {
return fetchAPI(endpoint, 'GET', header)
.then((json) => {
dispatch({ type: t.SCHEDULE, schedules: json.datas.description });
resultCB(json.schedules)
})
.catch((error) => {
dispatch({ type: types.EMPTY_SCHEDULE });
resultCB(error)
})
}
}
this is where i put my picker
export const mapStateToProps = state => ({
token: state.authReducer.token,
message: state.authReducer.message,
schedules: state.authReducer.schedules
});
export const mapDispatchToProps = (dispatch) => ({
actionsAuth: bindActionCreators(authAction, dispatch)
});
class Change extends Component {
constructor(){
super();
this.state={
staffId: "",
schedule: '',
type_absen: 1,
schedules: null
}
}
componentDidMount(){
this.props.actionsAuth.getSchedule(this.props.token);
}
render() {
return (
<View style={styles.picker}>
<Picker
selectedValue={this.state.schedule}
style={{backgroundColor:'white'}}
onValueChange={(sch) => this.setState({schedule: sch})}>
{this.state.schedules.map((l, i) => {
return <Picker.Item value={l} label={i} key={i} /> })}
</Picker>
</View>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Change);
This isn’t a React Native specific error. You initialized schedules to null so on first render, you try to call .map on null. That’s what is causing your error.
You fetch your data correctly in componentDidMount but that lifecycle method will fire after the initial render.
One common way to fix this is to initialize schedules to an empty array.
First initialise schedules: [] in the state with empty array, not with the null.
Fetching data in componentDidMount() is correct. ComponentDidMount() will be called after the first render of component so you have to update the state in the component from the updated store.
you can check whether props is changing or not in componentWillReceiveProps (depreciated) or in the latest alternative of componentWillReceiveProps method that is getDerivedStateFromProps().
Below is the syntax for both
componentWillReceiveProps(nextProps) {
if (this.props.schedules !== nextProps.schedules) {
this.setState({ schedules: nextProps.schedules });
}
}
static getDerivedStateFromProps(nextProps, prevState){
if (nextProps.schedules !== prevState.schedules) {
return { schedules: nextProps.schedules };
}
else return null; // Triggers no change in the state
}
Make sure your component should connected to store using connect
I'm setting up authentication flow in my React Native app, and I can pretty much get everything to work, except I can't figure out the actual proper place to navigate to the logged in stack with this.props.navigation.navigate("Main");.
I have a Switch Navigator at the top of my app rendering the auth stack (its own switch nav) and the main stack:
// App.js
const AppContainer = createAppContainer(
createSwitchNavigator({
Auth: AuthNavigator,
Main: MainTabNavigator,
})
);
export default class App extends Component {
render() {
return (
<Provider store={store}>
<View style={styles.container}>
<AppContainer />
</View>
</Provider>
);
}
}
The login screen is a complete dummy setup for now (it calls a local api and succeeds no matter what). Note the conditional at the start of the render method:
// LoginView.js
class LoginView extends Component {
state = { username: "", password: "" };
handleLogin() {
this.props.login.call(this);
}
render() {
if (this.props.user) {
this.props.navigation.navigate("Main");
return null;
}
return (
<View style={styles.container}>
<Overlay>
<View style={styles.modalContainer}>
<DotIndicator color={"darkgrey"} />
<Text>Logging in...</Text>
</View>
</Overlay>
// ... input components ...
<Button
title="Login"
onPress={this.handleLogin.bind(this)}
/>
</View>
);
}
}
export default connect(
state => ({
isLoading: state.auth.isLoading,
user: state.auth.user,
error: state.auth.error
}),
{ login }
)(LoginView);
Now, this technically works but I get the error Warning: Cannot update during an existing state transition (such as withinrender). Render methods should be a pure function of props and state..
I understand this to be thrown because I'm calling the navigate inside of the render. Makes sense.
I've tried navigating from my login action:
export function login() {
return dispatch => {
dispatch({ type: "LOGIN_START" });
fetch(url) // this will need to be a POST session (not a GET user)
.then(res => res.json())
.then(users => {
dispatch({ type: "LOGIN_SUCCESS", payload: users[0] });
console.log("Login succeeded");
this.props.navigation.navigate("Main");
})
.catch(error => {
console.warn("login failed");
dispatch({ type: "LOGIN_FAILURE", payload: error });
});
};
}
And it works but of course is very wrong in terms of separation of concerns, and this kind of solution also doesn't really stretch as far when I try use it for handling login failure.
I've also thought about doing it in my handleLogin() function (called by my login button), with and without async/await:
async handleLogin() {
await this.props.login.call(this);
console.log("Navigating to main screen");
this.props.navigation.navigate("Main");
}
But the navigation happens before the action finishes and the user is logged in, which is of course unacceptable once the auth flow is real. (Plus, when I do it with async/await, "Login Succeeded" gets logged twice! Beats me). It does feel like I'm on the right track doing it in my handleLogin() function.
Where do I put my navigate call?
I appear to have found something that works, but it takes steps away from Redux, and that makes me slightly uncomfortable.
I've taken all the fetching out of the Redux action and put it right in my LoginView. I'm considering this okay in Redux terms, because the only place the program needs to know about the authorization process and state is in the authorization flow itself (in the LoginView itself, for now). The only thing the store and the rest of the app need to know is the info for the user.
So I've replaced my Redux login action with a simple assignUser action. I've refactored the fetching into performLogin, which I call from the old handleLogin, after posting a state change to activate my "Logging in..." overlay.
// authActions.js
export function assignUser(user) {
return dispatch => {
console.log("user", user);
dispatch({ type: "LOGIN_SUCCESS", payload: user });
};
}
// from LoginView.js
performLogin() {
fetch("http://127.0.0.1:3000/users") // this will need to be a POST session (not a GET user)
.then(res => res.json())
.then(users => {
console.log("Login succeeded");
this.props.assignUser(users[0]);
console.log("Navigating to main screen");
this.props.navigation.navigate("Main");
})
.catch(error => {
console.warn("login failed", error);
this.setState({isLoading: false})
});
}
handleLogin() {
this.setState({ isLoading: true }, this.performLogin);
}
The rest of the LoginView component is the same as in my original post.
I'd still love to know if anyone has any methods for accomplishing this that keep the actions fully within Redux.