TypeError with .map and The Movie Database - reactjs

I am learning programming since a few months and I am starting to practice with some projects that I could create by myself or that I could find on the internet to study and learn more about different topics. In this case, I am working with React.js, Material UI and The Movie Database trying to make some kind of website related to movies and series. I was doing fine, until I needed to display all the genres and filter them on the page.
My intention is to display all the genres from The Movie Database and filter them when I needed. In some cases this code works fine and without any issue but in general it says that "genres.map is not a function" and the component doesn't render.
I know that this type of error normally happens when I try to map something that it is not an array. However, in this case genres IS an array. The most annoying thing for me is that in some rare cases the code works fine.
As I said before, I am new relatively new to programming and it is my first question here so I apologize if this is not the place to ask this. I would be very grateful if someone could help me to solve this and most important to understand what is the problem. Many thanks in advance!
import { Chip } from "#mui/material";
import axios from "axios";
import React, { useEffect } from "react";
const Genres = ({
selectedGenres,
setSelectedGenres,
genres,
setGenres,
type,
setPage,
}) => {
const addGenre = (genre) => {
setSelectedGenres([...selectedGenres, genre]);
setGenres(genres.filter((g) => g.id !== genre.id));
setPage(1);
};
const removeGenre = (genre) => {
setSelectedGenres(
selectedGenres.filter((selected) => selected.id !== genre.id)
);
setGenres([...genres, genre]);
setPage(1);
};
const fetchGenres = async () => {
const { data } = await axios.get(
`https://api.themoviedb.org/3/genre/${type}/list?api_key=${process.env.REACT_APP_API_KEY}&language=en-US`
);
setGenres(data.genres);
};
useEffect(() => {
fetchGenres();
return () => {
setGenres({});
};
// eslint-disable-next-line
}, []);
console.log(genres);
return (
<div style={{ padding: "6px 0" }}>
{selectedGenres.map((genre) => (
<Chip
label={genre.name}
style={{ margin: 2 }}
size="small"
key={genre.id}
clickable
onDelete={() => removeGenre(genre)}
/>
))}
{genres.map((genre) => (
<Chip
label={genre.name}
style={{ margin: 2 }}
size="small"
key={genre.id}
clickable
onClick={() => addGenre(genre)}
/>
))}
</div>
);
};
export default Genres;

genres must either consistently be an array, or your code must handle cases that are not an array. In your case the problem is here:
return () => {
setGenres({});
};
This will cause the state to be set to an object every time this component unmounts. Objects are not an array, and so do not have the functions of an array. When the component mounts again, it will be an object still for a brief period, before it fetches from the API and it gets set back to an array again.
Instead, use:
return () => {
setGenres([]);
};
Additionally, ensure that the default state where genres is initially declared (probably in the parent, or some ancestor) is set to an array otherwise it could be some other type before they are fetched from the API:
const [genres, setGenres] = useState([])
Off-topic but I'd also question if it's necessary to set it back to an empty array on unmount in the first place. Doing nothing would mean the old data is still there when this component remounts and the user would not need to wait for it to fetch again. The new data would replace once it arrives from the API. You could always implement a loading state to tell the user it is refreshing whilst not blocking them from seeing the "stale" data.

Related

Unable to prevent Flatlist from re-rendering (already using useCallback and memo)

I'm building a to do list app as part of a coding course, using Firebase Realtime Database and React Native with Expo.
I have no problems rendering the to do list, and in this case clicking a checkbox to indicate whether the task is prioritized or not.
However, each time I click on the checkbox to change the priority of a single task in the to do list, the entire Flatlist re-renders.
Each task object is as follows:
{id: ***, text: ***, priority: ***}
Task Component: (It consists of the text of the to do (task.text), and also a checkbox to indicate whether the task is prioritized or not). I've wrapped this component in React.memo, and the only props passed down from Todolist to Task are the individual task, but it still re-renders every time. (I left out most of the standard imports in the code snippet below)
import { CheckBox } from '#rneui/themed';
const Task = ({
item,
}) => {
console.log(item)
const { user } = useContext(AuthContext);
const onPressPriority = async () => {
await update(ref(database, `users/${user}/tasks/${item.id}`), {
priority: !item.priority,
});
};
return (
<View
style={{ flexDirection: 'row', alignItems: 'center', width: '95%' }}
>
<View
style={{ width: '90%' }}
>
<Text>{item.text}</Text>
</View>
<View
style={{ width: '10%' }}
>
<CheckBox
checked={item.priority}
checkedColor="#00a152"
iconType="material-community"
checkedIcon="checkbox-marked"
uncheckedIcon={'checkbox-blank-outline'}
onPress={onPressPriority}
/>
</View>
</View>
)}
export default memo(Task, (prevProps, nextProps) => {
if (prevProps.item !== nextProps.item) {
return true
}
return false
})
To Do List parent component: Contains the Flatlist which renders a list of the Task components. It also contains a useEffect to update the tasks state based on changes to the Firebase database, and the function (memoizedOnPressPriority) to update the task.priority value when the Checkbox in the task component is clicked. Since memoizedOnPressPriority is passed a prop to , I've tried to place it in a useCallback, but is still re-rendering all items when the checkbox is clicked. (I left out most of the standard imports in the code snippet below)
export default function Home2() {
const { user } = useContext(AuthContext);
const [tasks, setTasks] = useState([]);
useEffect(() => {
if (user) {
return onValue(ref(database, `users/${user}/tasks`), (snapshot) => {
const todos = snapshot.val();
const tasksCopy = [];
for (let id in todos) {
tasksCopy.push({ ...todos[id], id: id });
}
setTasks(tasksCopy);
});
} else {
setTasks([]);
}
}, [user]);
const renderItem = ({ item }) => (
<TaskTwo
item={item}
/>
);
return (
<View style={styles.container}>
<FlatList
data={tasks}
initialNumToRender={5}
windowSize={4}
renderItem={renderItem}
keyExtractor={item => item.id}
/>
</View>
);
}
Could anyone let me know what I'm doing wrong, and how I can prevent the entire Flatlist from re-rendering each time I invoke the memoizedOnPressPriority function passed down to the Task component from the TodoList parent component? Any help is much appreciated!
The flamegraph for the render is below:
Update: I moved the prioritize function (memoizedOnPressPriority) into the Task component and removed the useCallback - so it's not being passed as a prop anymore. The re-render still happens whenever I press it.
Update 2: I added a key extractor , and also a custom equality function into the memoized task component. Still keeps rendering!
I'm not familiar with Firebase Realtime Database, but if I understand the logic correctly, the whole tasks array is updated when one item changes, and this is what is triggering the list update.
Fixing the memo function
Wrapping the Task component in memo does not work because it performs a shallow comparison of the objects. The objects change each time the data is updated because a new tasks array with new objects is created, so the references of the objects are different.
See this post for more details.
To use memo, we have to pass a custom equality check function, that returns true if the component is the same with new props, like so:
export default memo(Task, (prevProps, nextProps) => {
if (prevProps.item.id === nextProps.item.id && prevProps.item.priority === nextProps.item.priority ) {
return true;
}
return false;
})
Note that is the text is modifiable, you'll want to check that too.
Alternative solution : read data from the Task component
This solution is recommended and takes full advantage of Firebase Realtime Database.
To update only the component that is updated, you need to pass an array of ids to your flatlist, and delegate the data reading to the child component.
It's a pattern I use with redux very often when I want to update a component without updating the whole flatlist.
I checked the documentation of Firebase Realtime Database, and they indeed encourage you to read data at the lowest level. If you have a large list with many properties, it's not performant to receive the whole list when only one item is updated. Under the hood, the front-end library manages the cache system automatically.
//TodoList parent Component
...
const [tasksIds, setTasksIds] = useState([]);
useEffect(() => {
if (user) {
return onValue(ref(database, `users/${user}/tasks`), (snapshot) => {
const todos = snapshot.val();
// Build an array of ids
const tasksIdsFromDb = todos.map((todo) => todo.id);
setTasksIds(tasksCopy);
});
} else {
setTasksIds([]);
}
}, [user]);
...
// keep the rest of the code and pass tasksIds instead of tasks to the flatlist
const Task = ({ taskId, memoizedOnPressPriority }) => {
const [task, setTask] = useState(null)
const { user } = useContext(AuthContext);
useEffect(() => {
if (user) {
// retrieve data by id so only the updated component will rerender
// I guess this will be something like this
return onValue(ref(database, `users/${user}/tasks/${taskId}`), (snapshot) => {
const todo = snapshot.val();
setTask(todo);
});
} else {
setTask(null);
}
}, [user]);
if (task === null) {
return null
}
// return the component like before

Single responsibility in React component

I was learning Single responsibility principle in React and created such component:
import React from "react";
import {useGetRemoteData} from "./useGetRemoteData";
export const SingleResponsibilityPrinciple = () => {
const {filteredUsers , isLoading} = useGetRemoteData()
const showDetails = (userId) => {
const user = filteredUsers.find(user => user.id===userId);
alert(user.contact)
}
return <>
<div> Users List</div>
<div> Loading state: {isLoading? 'Loading': 'Success'}</div>
{filteredUsers.map(user => {
return <div key={user.id} onClick={() => showDetails(user.id)}>
<div>{user.name}</div>
<div>{user.email}</div>
</div>
})}
</>
}
As you can see above, I have this code
const user = filteredUsers.find(user => user.id===userId);
The question is Is it best practice that if whenever we use map, reduce or any array function in React component, we should extract that logic out of a component, that is, filteredUsers.find(user => user.id===userId); should be extracted and we need to create utility function. So, function should not care about how a particular thing is done. Is it true?
Thank you for your question. I want to advice as follows
It is better for your to check if filteredUsers exists or not in your return.
{filteredUsers?.map(user=>{
//your code
})
You don't need to get specific user as you already loop in your map method. Just simply do something like this
{filteredUsers.map(user => {
return <div key={user.id} onClick={() => showDetails(alert(user.contact))}>
<div>{user.name}</div>
<div>{user.email}</div>
</div>
})}
Remember the difference between find, filter method of Javascript array. If you have unique value according to userId, you simply use find method to get the first value not array, if you use filter, you get arrays of the condition. Are you sure you don't need to alert(user[0].contact), not alert(user.contact)?

React native: useState not updating correctly

I'm new to react native and currently struggling with an infinite scroll listview. It's a calendar list that need to change depending on the selected company (given as prop). The thing is: the prop (and also the myCompany state are changed, but in the _loadMoreAsync method both prop.company as well as myCompany do hold their initial value.
import * as React from 'react';
import { FlatList } from 'react-native';
import * as Api from '../api/api';
import InfiniteScrollView from 'react-native-infinite-scroll-view';
function CalenderFlatList(props: { company: any }) {
const [myCompany, setMyCompany] = React.useState(null);
const [data, setData] = React.useState([]);
const [canLoadMore, setCanLoadMore] = React.useState(true);
const [startDate, setStartDate] = React.useState(undefined);
let loading = false;
React.useEffect(() => {
setMyCompany(props.company);
}, [props.company]);
React.useEffect(() => {
console.log('set myCompany to ' + (myCompany ? myCompany.name : 'undefined'));
_loadMoreAsync();
}, [myCompany]);
async function _loadMoreAsync() {
if ( loading )
return;
loading = true;
if ( myCompany == null ) {
console.log('no company selected!');
return;
} else {
console.log('use company: ' + myCompany.name);
}
Api.fetchCalendar(myCompany, startDate).then((result: any) => {
// code is a little more complex here to keep the already fetched entries in the list...
setData(result);
// to above code also calculates the last day +1 for the next call
setStartDate(lastDayPlusOne);
loading = false;
});
}
const renderItem = ({ item }) => {
// code to render the item
}
return (
<FlatList
data={data}
renderScrollComponent={props => <InfiniteScrollView {...props} />}
renderItem={renderItem}
keyExtractor={(item: any) => '' + item.uid }
canLoadMore={canLoadMore}
onLoadMoreAsync={() => _loadMoreAsync() }
/>
);
}
What I don't understand here is why myCompany is not updating at all in _loadMoreAsync while startDate updates correctly and loads exactly the next entries for the calendar.
After the prop company changes, I'd expect the following output:
set myCompany to companyName
use company companyName
But instead i get:
set myCompany to companyName
no company selected!
I tried to reduce the code a bit to strip it down to the most important parts. Any suggestions on this?
Google for useEffect stale closure.
When the function is called from useEffect, it is called from a stale context - this is apparently a javascript feature :) So basically the behavior you are experiencing is expected and you need to find a way to work around it.
One way to go may be to add a (optional) parameter to _loadMoreAsync that you pass from useEffect. If this parameter is undefined (which it will be when called from other places), then use the value from state.
Try
<FlatList
data={data}
renderScrollComponent={props => <InfiniteScrollView {...props} />}
renderItem={renderItem}
keyExtractor={(item: any) => '' + item.uid }
canLoadMore={canLoadMore}
onLoadMoreAsync={() => _loadMoreAsync() }
extraData={myCompany}
/>
If your FlatList depends on a state variable, you need to pass that variable in to the extraData prop to trigger a re-rendering of your list. More info here
After sleeping two nights over the problem I solved it by myself. The cause was an influence of another piece of code that used React.useCallback(). And since "useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed" (https://reactjs.org/docs/hooks-reference.html#usecallback) the code worked with the old (or initial) state of the variables.
After creating the whole page new from scratch I found this is the reason for that behavior.

ReactJS - Alternative to passing useState between files

Background
I have a file that presents my main page Dash.js
This presents some data from an API on a "card", from two other files List.js and ListLoading.js
I have an additional "card" which I can trigger open with default useState value of 1, and the onClick works to close, as you will see in the dash.js file.
Current Code
//Dash.js
function Dash(props) {
//control additional card
const [openCard, setopenCard] = React.useState(0);
const closeCard = () => {
setopenCard(0);
}
//set API repo
const apiUrl = (`http://example.com/api/`);
axios.get(apiUrl, {
withCredentials: true }).then((res) =>{
setAppState({ loading: false, repos: res.data.emails });
});
return (
{(openCard>0 &&
<Card>
<Cardheader onClick={() => closeCard()}>
Click here to close
</Cardheader>
<Cardbody>
Some data here
</Cardbody>
</Card>
)
|| null
}
<Card>
<ListLoading isLoading={appState.loading} repost={appState.repos} />
<Card>
);
}
//List.js
const List = (props) => {
const { repos } = props;
if (!repos || repos.length === 0) return <p>No data available</p>;
for (var key in repos) {
return (
{repos.map((repo) => {
return (
<p className='repo-text max-width' >ID:{repo.id}{" "}Value:{repo.value} </p>
);}
)}
);}
};
export default List;
//ListLoading.js
function WithListLoading(Component) {
return function WihLoadingComponent({ isLoading, ...props }) {
if (!isLoading) return <Component {...props} />;
return (
<p style={{ textAlign: 'center', fontSize: '30px' }}>
Fetching data may take some time, please wait
</p>
);
};
}
export default WithListLoading;
Desired Outcome
I want to set the the value for openCard.useState() to the repos.id.
e.g. onClick={() => openCard({repos.id})}
The complication of this is that I need to retrieve that code from List.js and pass it to the useState for the openCard, which is in Dash.js.
I am still fairly new to react so this is proving a little tricky to work out how to do.
What I've tried
I have looked into useContext, but either it has confused me or I am right to think this would not work for what I am trying to do.
I have looked into redux, however this seems like that may be overkill for this solution.
I have tried a series of passing the different constants via import/export however I now understand that useState is not designed to work this way and should really be used within the function/class where it is contained.
So any thoughts to remedy would be greatly appreciated!
So, just to restate what I understood your issue to be:
You have a parent component that renders a list of objects and can render a detail card of one of the object.
You want to have a single item in your list of objects be able to tell the parent "please open card 123".
Now to look at the options you considered:
Redux I agree Redux is overkill for this. Redux is usually only necessary if you need complex, possibly async reading and writing to a single shared datasource across the whole scope of your application. For a little UI interaction like this, it is definitely not worth setting up Redux.
React Context Context relies on a Provider component, which you wrap some chunk of your app in. Any component below that Provider can then use useContext to reach into the memory of that Provider. You can store anything in there that you could store in a component, from a single state variable up to a more complex useReducer setup. So, in a way, this basically does what you were hoping to do with static variables passing the state around. This is the right solution if you were going to be using this state value across a wide variety of components.
Props are probably the right way to go here - since you have a parent who wants to get messages from a child directly you can give the child a callback function. This is the same as the onClick function you can give a button, except here you can pass your list a onShowCard function.
In your Dash:
<ListLoading
isLoading={appState.loading} repost={appState.repos}
onShowCard={(cardId) => setopenCard(cardId)} />
At the end of the List:
{repos.map((repo) => {
return (
<button key={repo.id} className='repo-text max-width' onClick={() => { props.onShowCard(repo.id) }>
ID:{repo.id}{" "}Value:{repo.value}
</button>
);}
)}
You can pass on the function to update state to ListLoading component which will be forwarded to List component assuming it is wrapped by thee HOC WithListLoading.
Inside List you can then attach and onClick on the element to pass on the id of the clicked element
function Dash(props) {
//control additional card
const [openCard, setopenCard] = React.useState(0);
const closeCard = () => {
setopenCard(0);
}
//set API repo
const apiUrl = (`http://example.com/api/`);
axios.get(apiUrl, {
withCredentials: true
}).then((res) =>{
setAppState({ loading: false, repos: res.data.emails });
});
const handleOpen = id => {
setopenCard(id);
}
return (
{(openCard>0 &&
<Card>
<Cardheader onClick={() => closeCard()}>
Click here to close
</Cardheader>
<Cardbody>
Some data here
</Cardbody>
</Card>
)
|| null
}
<Card>
<ListLoading isLoading={appState.loading} repost={appState.repos} handleOpen={handleOpen} />
<Card>
);
}
const List = (props) => {
const { repos, handleOpen } = props;
if (!repos || repos.length === 0) return <p>No data available</p>;
for (var key in repos) {
return (
{repos.map((repo) => {
return (
<p className='repo-text max-width' onClick={() => props.handleOpen(repo.id)} >ID:{repo.id}{" "}Value:{repo.value} </p>
);}
)}
);}
};
export default List;

Refactoring class component to functional component with hooks, getting Uncaught TypeError: func.apply is not a function

This is my first attempt to refactor code from a class component to a functional component using React hooks. The reason we're refactoring is that the component currently uses the soon-to-be-defunct componentWillReceiveProps lifecylcle method, and we haven't been able to make the other lifecycle methods work the way we want. For background, the original component had the aforementioned cWRP lifecycle method, a handleChange function, was using connect and mapStateToProps, and is linking to a repository of tableau dashboards via the tableau API. I am also breaking the component, which had four distinct features, into their own components. The code I'm having issues with is this:
const Parameter = (props) => {
let viz = useSelector(state => state.fetchDashboard);
const parameterSelect = useSelector(state => state.fetchParameter)
const parameterCurrent = useSelector(state => state.currentParameter)
const dispatch = useDispatch();
let parameterSelections = parameterCurrent;
useEffect(() => {
let keys1 = Object.keys(parameterCurrent);
if (
keys1.length > 0 //if parameters are available for a dashboard
) {
return ({
parameterSelections: parameterCurrent
});
}
}, [props.parameterCurrent])
const handleParameterChange = (event, valKey, index, key) => {
parameterCurrent[key] = event.target.value;
console.log(parameterCurrent[key]);
return (
prevState => ({
...prevState,
parameterSelections: parameterCurrent
}),
() => {
viz
.getWorkbook()
.changeParameterValueAsync(key, valKey)
.then(function () {
Swal.fire({
position: "center",
icon: "success",
title:
JSON.stringify(key) + " set to " + JSON.stringify(valKey),
font: "1em",
showConfirmButton: false,
timer: 2500,
heightAuto: false,
height: "20px"
});
})
.otherwise(function (err) {
alert(
Swal.fire({
position: "top-end",
icon: "error",
title: err,
showConfirmButton: false,
timer: 1500,
width: "16rem",
height: "5rem"
})
);
});
}
);
};
const classes = useStyles();
return (
<div>
{Object.keys(parameterSelect).map((key, index) => {
return (
<div>
<FormControl component="fieldset">
<FormLabel className={classes.label} component="legend">
{key}
</FormLabel>
{parameterSelect[key].map((valKey, valIndex) => {
console.log(parameterSelections[key])
return (
<RadioGroup
aria-label="parameter"
name="parameter"
value={parameterSelections[key]}
onChange={(e) => dispatch(
handleParameterChange(e, valKey, index, key)
)}
>
<FormControlLabel
className={classes.formControlparams}
value={valKey}
control={
<Radio
icon={
<RadioButtonUncheckedIcon fontSize="small" />
}
className={clsx(
classes.icon,
classes.checkedIcon
)}
/>
}
label={valKey}
/>
</RadioGroup>
);
})}
</FormControl>
<Divider className={classes.divider} />
</div>
);
})
}
</div >
)};
export default Parameter;
The classes const is defined separately, and all imports of reducers, etc. have been completed. parameterSelect in the code points to all available parameters, while parameterCurrent points to the default parameters chosen in the dashboard (i.e. what the viz initially loads with).
Two things are happening: 1. Everything loads fine on initial vizualization, and when I click on the Radio Button to change the parameter, I can see it update on the dashboard - however, it's not actually showing the radio button as being selected (it still shows whichever parameter the viz initialized with as being selected). 2. When I click outside of the Filterbar (where this component is imported to), I get Uncaught TypeError: func.apply is not a function. I refactored another component and didn't have this issue, and I can't seem to determine if I coded incorrectly in the useEffect hook, the handleParameterChange function, or somewhere in the return statement. Any help is greatly appreciated by this newbie!!!
This is a lot of code to take in without seeing the original class or having a code sandbox to load up. My initial thought is it might be your useEffect
In your refactored code, you tell your useEffect to only re-run when the props.parameterCurrent changes. However inside the useEffect you don't make use of props.parameterCurrent, you instead make use of parameterCurrent from the local lexical scope. General rule of thumb, any values used in the calculations inside a useEffect should be in the list of re-run dependencies.
useEffect(() => {
let keys1 = Object.keys(parameterCurrent);
if (
keys1.length > 0 //if parameters are available for a dashboard
) {
return ({
parameterSelections: parameterCurrent
});
}
}, [parameterCurrent])
However, this useEffect doesn't seem to do anything, so while its dependency list is incorrect, I don't think it'll solve the problem you are describing.
I would look at your dispatch and selector. Double check that the redux store is being updated as expected, and that the new value is making it from the change callback, to the store, and back down without being lost due to improper nesting, bad key names, etc...
I'd recommend posting a CodeSandbox.io link or the original class for further help debugging.

Resources