Still a newbie, so not sure how to solve this issue. The app gets data about movie genres from an API, it then uses those genres to create options in the drop-down selector. The user can choose a type (tv show or movie) and then the genre. When they hit search it will return a random movie or show in the genre. The starting values are tv show and action. I want the user to be able to immediately hit search and find a title to watch. My problem is the data about movies/ shows in the specified type and genre are only fetched when the user changes the selector option from the default one. You can see this hosted on GH Pages here or check the GH repository
So I want the data from the full_url to be fetched upon render. The feedback from the console is that upon render chosenType and chosenGenre are undefined in the fetch method fetchMovieList(). But once I change the type, an array of movies or shows are fetched.
Any help or advice would be appreciated.
Below is the code.
import { createContext, useState, useEffect } from "react";
export const MovieContext = createContext({});
export function MovieProvider({ children }) {
const [movieList, setMovieList] = useState([]);
const [randomMovie, setRandomMovie] = useState({});
const [hasMovie, setHasMovie] = useState(false);
const [genreOptions, setGenreOptions] = useState([]);
const [chosenGenre, setChosenGenre] = useState();
const [typeOptions, setTypeOptions] = useState([]);
const [chosenType, setChosenType] = useState();
const api = "api_key=3fa371d07ffd6838dc488ff081631c5d";
const genres_url =
"https://api.themoviedb.org/3/genre/movie/list?api_key=3fa371d07ffd6838dc488ff081631c5d&language=en-US";
const types = [
{ type: "TV Show", value: "tv" },
{ type: "Movie", value: "movie" },
];
//fetching genres from API to use in selector and for searching and setting types for selector
const fetchGenres = () => {
fetch(genres_url)
.then((res) => res.json())
.then((data) => {
setChosenGenre(data.genres[0].id);
setGenreOptions(data.genres);
setTypeOptions(types);
setChosenType(types[0].value);
console.log(data);
});
};
//getting genres when page loads
useEffect(() => {
fetchGenres();
}, []);
//setting the value of slelector drop downs
const onChangeGenre = (e) => {
setChosenGenre(e.target.value);
};
const onChangeType = (e) => {
setChosenType(e.target.value);
};
//fetching movies or shows from the selected genre
const full_url = `https://api.themoviedb.org/3/discover/${chosenType}?${api}&with_genres=${chosenGenre}`;
const fetchMovieList = () => {
fetch(full_url)
.then((res) => res.json())
.then((data) => {
setMovieList(data.results);
console.log(data.results);
});
};
console.log(chosenType, chosenGenre)
//fetches data from API when type or genre is changed
useEffect(() => {
fetchMovieList();
}, [chosenType, chosenGenre]);
//function that selects a random movie or show from the already fetched data
const getRandomMovie = (e) => {
e.preventDefault();
const randomItem = movieList[Math.floor(Math.random() * movieList.length)];
setRandomMovie(randomItem);
console.log(randomItem);
setHasMovie(true);
};
//passing state and functions to child components
return (
<MovieContext.Provider
value={{
getRandomMovie,
onChangeGenre,
onChangeType,
randomMovie,
hasMovie,
genreOptions,
chosenGenre,
typeOptions,
chosenType,
types,
}}
>
{children}
</MovieContext.Provider>
);
}
The problem is here:
//fetches data from API when type or genre is changed
useEffect(() => {
fetchMovieList();
}, [chosenType, chosenGenre]);
The useEffect hook will be called every time the dependencies change, but also on the initial render of the component. At first, the chosenType and chosenGenre will still be their initial value null. You can fix it with a simple fix like this:
//fetches data from API when type or genre is changed
useEffect(() => {
if(!chosenType || !chosenGenre) return;
fetchMovieList();
}, [chosenType, chosenGenre]);
You can try setting the selected attribute to the dropdown, as shown below. Then your api will have the values to make the request, when the page loads.
Note : This will set the last iterating option as the selected value.
Form.js
<form className={classes["form-row"]} onSubmit={getRandomMovie}>
<p className={classes.label}>Type: </p>
<select value={chosenType} onChange={onChangeType}>
{typeOptions.map((type) => (
<option key={type.value} value={type.value} selected> // <== Here
{type.type}
</option>
))}
</select>
<p className={classes.label}>Genre: </p>
<select value={chosenGenre} onChange={onChangeGenre}>
{genreOptions.map((option) => (
<option key={option.id} value={option.id} selected> // <== Here
{option.name}
</option>
))}
</select>
<button>Search</button>
</form>;
I think the problem is that the default values of chosenGenre are undefined. Set the default values for chosenGenre.
const [chosenGenre, setChosenGenre] = useState("tv");
Solved this issue. It was because I used the API incorrectly, had to use a different url for the tv show type, as opposed to a movie type.
Related
I've created two components which together create a 'progressive' style input form. The reason I've chosen this method is because the questions could change text or change order and so are being pulled into the component from an array stored in a JS file called CustomerFeedback.
So far I've been trying to add a data handler function which will be triggered when the user clicks on the 'Proceed' button. The function should collect all of the answers from all of the rendered questions and store them in an array called RawInputData. I've managed to get this to work in a hard coded version of SurveyForm using the code shown below but I've not found a way to make it dynamic enough to use alongside a SurveyQuestion component. Can anybody help me make the dataHander function collect data dynamically?
There what I have done:
https://codesandbox.io/s/angry-dew-37szi2?file=/src/InputForm.js:262-271
So, we can make it easier, you just can pass necessary data when call handler from props:
const inputRef = React.useRef();
const handleNext = () => {
props.clickHandler(props.reference, inputRef.current.value);
};
And merge it at InputForm component:
const [inputData, setInputData] = useState({});
const handler = (thisIndex) => (key, value) => {
if (thisIndex === currentIndex) {
setCurrentIndex(currentIndex + 1);
setInputData((prev) => ({
...prev,
[key]: value
}));
}
};
// ...
<Question
// ...
clickHandler={handler(question.index)}
/>
So, you wanted array (object more coninient I think), you can just save data like array if you want:
setInputData(prev => [...prev, value])
Initially, I thought you want to collect data on button clicks in the InputForm, but apparently you can do without this, this solution is simpler
UPD
Apouach which use useImperativeHandle:
If we want to trigger some logic from our child components we should create handle for this with help of forwarfRef+useImperativeHandle:
const Question = React.forwardRef((props, ref) => {
const inputRef = React.useRef();
React.useImperativeHandle(
ref,
{
getData: () => ({
key: props.reference,
value: inputRef.current.value
})
},
[]
);
After this we can save all of our ref in parent component:
const questionRefs = React.useRef(
Array.from({ length: QuestionsText.length })
);
// ...
<Question
key={question.id}
ref={(ref) => (questionRefs.current[i] = ref)}
And we can process this data when we want:
const handleComplete = () => {
setInputData(
questionRefs.current.reduce((acc, ref) => {
const { key, value } = ref.getData();
return {
...acc,
[key]: value
};
}, {})
);
};
See how ref uses here:
https://reactjs.org/docs/forwarding-refs.html
https://reactjs.org/docs/hooks-reference.html#useimperativehandle
I still strongly recommend use react-hook-form with nested forms for handle it
Im new to react and I have a question about select component of material ui.
The thing is like this, I have a funcionality that is for creating and editing an User, this User is an object, it has primary key and some data, between this data there is a relation with other object that is a role, so in this case I use a Select component to select the role.
So, I have the role list that I bring from the backend:
const [rolList, setRolList] = React.useState([]);
const searchRoles = async () => {
try {
setRolList(await api.post('/v1/roles/buscar', filtroRol));
} catch (error) {
snackbar.showMessage(error, "error");
}
}
And the select component that is inside a formik:
<Mui.Select
label="Role"
value={values.usuRolPk}
onChange={(opt) =>{handleChange(opt);
}}
>
<Mui.MenuItem disabled key={0} value=''>
Select role
</Mui.MenuItem>
{rolList.map((e) => {
return <Mui.MenuItem key={e.rolPk} value={e.rolPk}>{e.rolName}</Mui.MenuItem>;
})}
</Mui.Select>
As you can see, for the value of the select I use the pk of role, so when the user is persisted I have to search in the list of roles and atach the selected object to the users object and send it to the backend.
Something like this (usuRolPk is the value of the select, usuRol is the relation with object role):
const save = async (values) => {
try {
if(values.usuRolPk==null){
values.usrRole=null;
}else{
values.usrRole=rolList.filter(element=>''+element.rolPk==values.usuRolPk)[0];
}
...
if (values.usrPk == null) {
await api.post('/v1/users', values);
} else {
await api.put('/v1/users/' + values.usrPk, values);
}
handleClose();
snackbar.showMessage("GUARDADO_CORRECTO", "success")
} catch (error) {
snackbar.showMessage(error, 'error');
}
return;
}
The thing is, I want to skip that last step of having to search in the list of roles with the selected Pk.
Is there a way of working just with the object as the selected value instead of the pk? I tried just changing the value to have the whole object like this:
<Mui.Select
label="role"
value={values.usuRol}
onChange={(opt) =>{handleChange(opt);
}}
>
<Mui.MenuItem disabled key={0} value=''>
Select role
</Mui.MenuItem>
{rolList.map((e) => {
return <Mui.MenuItem key={e.rolPk} value={e}>{e.rolName}</Mui.MenuItem>;
})}
</Mui.Select>
This works just when Im creating a new object, but when I try to edit an object that already exists and already has a role, when I pass the role to the select It says something like I initialize the Select with a value that doesnt exist in the list of roles.
Is there a way to achieve this?
Thanks!
Per conversation in the comments on the question:
I'm doing this with a normal select and options purely for convenience, but you can replace them easily enough with their mui equivalents:
import React, { useState, useEffect, useCallback } from 'react';
const SomeComponent = () => {
const [list, setList] = useState({}); // { "1": someObjWithId1, etc }
const [selected, setSelected] = useState();
useEffect(() => {
const getList = async () => {
const resp = await fetch('/some/url'); // get object list from backend
const data = await resp.json();
setList(data);
};
if (!list.length) getList();
}, []);
const handler = useCallback((evt) => {
setSelected(list[evt.target.value]);
}, [list, setSelected]);
return (
<select onChange={handler}>
{Object.entries(list).map(([id, obj]) => selected && id === selected.id
? <option selected key={id} value={id}>{obj.text}</option>
: <option key={id} value={id}>{obj.text}</option>
)}
</select>
);
};
The component will render a select element with the options once they've been passed from the backend. The change handler will update the state with the entire object (keyed by the id/value) selected. In real life you'd likely have the state in a parent form component and pass it with the setter through props, but you get the idea.
Hello I am building photo gallery where I would like to add feature that user will be able filter by Category. I tried some solutions but there are two bugs that I am not able to fix. First is that if I go to the GalleryPage (using Swtich) it does NOT render dynamically added buttons from FilterButton component. I have to click one more time on the link and then it DOES render the buttons. I dont know why it does not work on the first render.
Other issue is that I am able to filter by category but it causes the infinite loop in the useEffect and I dont know how to fix it.
I have got GalleryPage component where I am getting data from API and parsing the data for using later in other components. Here it seems that is all working fine.
const GalleryPage = () => {
const url = 'someurl';
const [data, setData] = useState([]);
const [categoryList, setCategoryList] = useState([]);
const [category, setCategory] = useState('All');
useEffect(() => {
const fetchData = async () => {
const result = await axios(url,);
setData(result.data)
result.data.forEach(item => {
imageUrl.push(item.image)
if (categoryList.indexOf(item.group) === -1) {
categoryList.push(item.group)
}
})
}
fetchData();
}, [])
return (
<FilterButton setCategory={setCategory} categoryList={categoryList}/>
<Gallery data={data} category={category}/>
)
}
If I go to the GalleryPage the h3 and 'All' button is rendered. But I have to click on the link one more time to render the buttons inside the map function:
const FilterButton = ({setCategory, categoryList}) => {
return(
<h3>Gallery</h3>
<button onClick={()=> setCategory('All')}>All</button>
{categoryList.map(item => (
<button key={item} onClick={()=> setCategory(item)}>{item}</button>
))}
)
};
export default FilterButton;
And here I am not able to fix the infinite loop:
const Gallery = ({data, category}) => {
const [photos, setPhotos] = useState([]);
useEffect(()=>{
let temp = []
if (category === 'All'){
setPhotos(data)
}else{
data.map(item => {
temp.push(item)
})
}
setPhotos(temp)
})
return(
photos.map((item =>
<img key={item.id} src={item.image}/>
))
)
};
export default Gallery;
If I add empty array to the useEffect it does not work at all. Also I am using styled components and framer motion but it should not have affect on this I hope.
First, I see that you're never setting your state for categoryList.
After modifying categoryList, you should call setCategoryList() with the new category list. This way, the state variable will be 'remembered' when the component is re-rendered.
You can read about the useState hook here.
Additionally, for the useEffect hook, the 'empty array' you pass in at the end is actually an array of variables to 'watch' for changes. If you pass an empty array, the useEffect will only run once, at the first page load. However, you can pass in something like [category] so that the useEffect is only called when the category variable is modified, which I persume is what you want to do.
i'm new in React and i'm developing an application with standard CRUD operations to a Express.js and MongoDB remote backend.
In a page I need to display values from a GET API call to a remote server, made with Axios. Every object as multiple fields, and the company field (the value in Exhibitor column, for example is something like 5f280eb605c9b25cfeee285c) corresponds to the _id Mongo object field value of another object in another collection.
I need to recover in the table raw the value, make another API call and get the name field (for example Company name example string) from the object with that _id. After that I need to display it in the table fields instead of the _id.
To make it more clear for example the item.company field 5f27e8ee4653de50faeb1784 will be showed as Company name example string.
Also I need to do the same with Status column (but without GET API call to a remote server), where I need to display an icon depending on item.active value, which is boolean.
This needs to be done without any button, but when I open the page automatically.
I've made a standard javascript function to do this but i'm getting an infinite loop, i suppose because React is calling the function every time he renders.
What is the correct way to do this operation?
Here's the errors from the console after the loop
xhr.js:178 GET http://myserver.com/companies/5f280eb605c9b25cfeee285c net::ERR_INSUFFICIENT_RESOURCES
import React, { useState, useEffect, useCallback } from 'react'
import { Tab, Tabs, Col, Form, Button } from 'react-bootstrap'
import { FiTrash, FiCloud, FiPhoneCall, FiUserCheck, FiUserX, FiEye } from 'react-icons/fi'
import axios from 'axios'
const EventProfile = (props) => {
// SOME CODE HERE //
//GET STANDS FROM DB
const [allStands, viewStands] = useState([{}]);
useEffect(() => {
const id = props.match.params.id
const fetchStands = async () => {
const response = await axios.get(`http://myserver.com/stands/${id}`);
viewStands(response.data);
}
fetchStands();
}, [])
// RECOVER NAME USING THE COMPANY ID FROM ANOTHER COLLECTION
const [companyNameGetted, getCompanyName] = useState({})
const getCompanyFromId = useCallback((props) => {
const id = props;
const getCompany = async () => {
const response = await axios.get(`http://myserver.com/companies/${id}`);
getCompanyName(response.data);
}
getCompany([]);
}, [])
// DISPLAY ICON DEPENDING ON OBJECT active FIELD
const handleStandStatus = (status) => {
if(status === true) {
return <FiCloud style={{color: "green"}}/>;
} else {
return <FiCloud style={{color: "grey"}} description="Offline"/>;
}
}
// OTHER CODE HERE //
return (
//SOME CODE HERE//
<Tab eventKey="stands" title="Stands">
<div className="py-12 w-full">
<table className="table table-lg">
<thead>
<tr>
<th>Status</th>
<th>Name</th>
<th>Exhibitor</th>
<th>Size</th>
<th>Color Palette</th>
</tr>
</thead>
<tbody>
{allStands.map((item, index) =>{
return(
<tr key={index}>
<td>{handleStandStatus(item.active)}</td>
<td><Link to={`/standProfile/${item._id}`}>{item.name}</Link></td>
<td>{getCompanyFromId(item.company)}<Link to={`/companyProfile/${item.company}`}><span>{companyNameGetted.name}</span></Link></td>
<td>{item.size}</td>
<td>{item.colorPalette}</td>
</tr>
)
})}
</tbody>
</table>
</div>
</Tab>
// OTHER CODE HERE //
)
}
export default EventProfile
Probably this part is responsible for the infinite loop:
<td>{getCompanyFromId(item.company)}<Link to={`/companyProfile/${item.company}`}><span>{companyNameGetted.name}</span></Link></td>
because you call a function within the return of your component, which the function then will call the getCompany function which will update your companyNameGetted state.
The companyNameGetted state is referenced on your component return , so calling the getCompanyFromId will result in a re-render, that will fetch the company, change the state, re-render, etc, resulting in an infinite loop.
You can fetch the companies within the useEffect after you get all the stands, or you can set a
useEffect(() => {get all company from allStands}, [allStands]);
so it'll reflect on allStands state changes.
Edit: here's an example to further describe what I mean.
const EventProfile = props => {
// usually you'll want to name the variables as so:
// a noun/object for the first one (stands)
// a setter for the second one, since it is a function to set the `stands`
const [stands, setStands] = useState([]);
const [companies, setCompanies] = useState({});
// usual useEffect that'll be triggered on component load, only one time
useEffect(() => {
const fetchStands = async () => {
const response = await axios.get("stands url here");
setStands(response.data);
};
fetchStands();
}, []);
//another useEffect that'll be triggered when there's a change in the dependency array given, i.e. the `stands` variable. so, it'll fetch company names whenever the `stands` state changes.
useEffect(() => {
const fetchCompanies = async () => {
const newCompanies = {...companies};
// wait for all company names have been retrieved
await Promise.all(stands.forEach(s => {
const id = s.company;
const response = await axios.get("company url here with " + id);
newCompanies[id] = response.data;
}));
setCompanies(newCompanies);
};
fetchCompanies();
}, [stands]);
return (
// ... some components
{stands.map((item, index) => (
<tr key={index}>
<td><Link to={`/some/url/${item.company}`}>{companies[item.company]}</Link></td>
</tr>
)}
);
}
This is my category state
const [category, setCategory] = useState('');
This's the form element:
<select onChange={e => setCategory(e.target.value)}>
<Options options={categoryList} value={category}/>
</select>
On changing the value, i'm getting category as selected
const handleBusinessInfoSubmit = (e) => {
try{
e.preventDefault();
console.log("category selected is " +category);
}
catch{
console.log("something went wrong!");
}
}
How do I setCategory state when the user doesn't change the value and hits Submit?
For reference sake, here is category list that will come as dynamic later in key value pair
const categoryList = [
{
id: 1,
value: 'Public Services'
}, {
id: 2,
value: 'Automotive'
}
];
// generate select dropdown option list dynamically
function Options({ options }) {
return (
options.map(option =>
<option key={option.id} value={option.value}>
{option.value}
</option>)
);
}
Probably I would add default initial value to useState as instead of '':
const [category, setCategory] = useState(categoryList[0]);
Or maybe if the data is coming dynamically then calling setCategory() with the value from the API result what you would like to have as default.
I hope this helps!