Ok, I got a head scratcher I need a little bit of help with. The setup is that I have React/Redux app with a Categories page that reads a list of categories from an API, then lists them out. That part works fine. What I'm trying to do is pass in an event handler to each of the category child components that, when clicked, dispatches an action that toggles the state of the component, i.e., if the category is selected and clicked on, it will "unselect" it (which actually means deleting an entry from a database table called user_category), and if not selected, will "select" that category for that user (add an entry in the user_category table).
So I've got an onclick handler (handleCatClick) that is supposed to ultimately pass a categoryId and a userId to perform these operations. Unfortunately what I'm finding that even though these arguments are being passed to the function, they end up being undefined. So I'm not sure if I'm passing this function correctly or what exactly I've missed.
Everything works other than this - maybe you can help me spot the problem ;-)
Click here to view the database layout
Click here to see how the category page looks
The applicable pages in my app:
The architecture looks basically like this:
/views/[Categories]
- index.js (wrapper for the Categories Component)
- CategoriesComponent.jsx (should be self-explanatory)
[duck]
- index.js (just imports a couple of files & ties stuff together)
- operations.js (where my handleCatClick() method is)
- types.js (Redux constants)
- actions.js (Redux actions)
- reducers.js (Redux reducers)
[components]
[Category]
- index.jsx (the individual Category component)
/views/index.js(main Category page wrapper)
import { connect } from 'react-redux';
import CategoriesComponent from './CategoriesComponent';
import { categoriesOperations } from './duck'; // operations.js
const mapStateToProps = state => {
// current state properties passed down to LoginComponent (LoginComponent.js)
const { categoryArray } = state.categories;
return { categoryArray }
};
const mapDispatchToProps = (dispatch) => {
// all passed in from LoginOperations (operations.js)
const loadUserCategories = () => dispatch(categoriesOperations.loadUserCategories());
const handleCatClick = () => dispatch(categoriesOperations.handleCatClick());
return {
loadUserCategories,
handleCatClick
}
};
const CategoriesContainer = connect(mapStateToProps,mapDispatchToProps)(CategoriesComponent);
export default CategoriesContainer;
/views/CategoriesComponent.jsx (display layer for the Categories view)
import React from 'react';
import {Row,Col,Container, Form, Button} from 'react-bootstrap';
import {Link} from 'react-router-dom';
import './styles.scss';
import Category from './components/Category';
import shortid from 'shortid';
class CategoriesComponent extends React.Component {
constructor(props) {
super(props);
this.loadUserCats = this.props.loadUserCategories;
this.handleCatClick = this.props.handleCatClick;
}
componentWillMount() {
this.loadUserCats();
}
render() {
return (
<Container fluid className="categories nopadding">
<Row>
<Col xs={12}>
<div className="page-container">
<div className="title-container">
<h4>Pick your favorite categories to contine</h4>
</div>
<div className="content-container">
<div className="category-container">
{
this.props.categoryArray.map((item) => {
return <Category className="category" handleClick={this.props.handleCatClick} key={shortid.generate()} categoryData={item} />
})
}
</div>
</div>
</div>
</Col>
</Row>
</Container>
)
}
}
export default CategoriesComponent
/views/Categories/components/index.jsx (Single Category Component)
import React from 'react';
import {Row,Col,Container, Form, Button} from 'react-bootstrap';
import './styles.scss';
import Img from 'react-image';
class Category extends React.Component {
constructor(props) {
super(props);
this.state = {
categoryName: this.props.categoryData.category_name,
categoryImg: this.props.categoryData.category_img,
categoryId: this.props.categoryData.category_id,
userId: this.props.categoryData.user_id,
selected: this.props.categoryData.user_id !== null,
hoverState: ''
}
this.hover = this.hover.bind(this);
this.hoverOff = this.hoverOff.bind(this);
this.toggleCat = this.toggleCat.bind(this);
}
toggleCat() {
// the onClick handler that is supposed to
// pass categoryId and userId. When I do a
// console.log(categoryId, userId) these two values
// show up no problem...
const {categoryId, userId} = this.state;
this.props.handleClick(categoryId, userId);
}
hover() {
this.setState({
hoverState: 'hover-on'
});
}
hoverOff() {
this.setState({
hoverState: ''
});
}
render() {
const isSelected = (baseCat) => {
if(this.state.selected) {
return baseCat + " selected";
}
return baseCat;
}
return (
<div className={"category" + ' ' + this.state.hoverState} onClick={this.toggleCat} onMouseOver={this.hover} onMouseOut={this.hoverOff}>
<div className={this.state.selected ? "category-img selected" : "category-img"}>
<Img src={"/public/images/category/" + this.state.categoryImg} className="img-fluid" />
</div>
<div className="category-title">
<h5 className={this.state.selected ? "bg-primary" : "bg-secondary"}>{this.state.categoryName}</h5>
</div>
</div>
);
}
}
export default Category;
/views/Categories/duck/operations.js (where I tie it all together)
// operations.js
import fetch from 'cross-fetch';
import Actions from './actions';
import Config from '../../../../config';
const loadCategories = Actions.loadCats;
const selectCat = Actions.selectCat;
const unSelectCat = Actions.unSelectCat;
const localState = JSON.parse(localStorage.getItem('state'));
const userId = localState != null ? localState.userSession.userId : -1;
const loadUserCategories = () => {
return dispatch => {
return fetch(Config.API_ROOT + 'usercategories/' + userId)
.then(response => response.json())
.then(json => {
dispatch(loadCategories(json));
});
}
}
const handleCatClick = (categoryId, categoryUserId) => {
// HERE IS WHERE I'M HAVING A PROBLEM:
// for whatever reason, categoryId and categoryUserId
// are undefined here even though I'm passing in the
// values in the Category component (see 'toggleCat' method)
var params = {
method: categoryUserId !== null ? 'delete' : 'post',
headers: {'Content-Type':'application/json'},
body: JSON.stringify(
{
"category_id": categoryId,
user_id: categoryUserId !== null ? categoryUserId : userId
}
)
};
const toDispatch = categoryUserId !== null ? unSelectCat : selectCat;
return dispatch => {
return fetch(Config.API_ROOT + 'usercategories/', params)
.then(response => response.json())
.then(json => {
dispatch(toDispatch(json));
});
}
}
export default {
loadUserCategories,
handleCatClick
}
The problem that I am having:
So I'm thinking I'm either not referencing handleCatClick correctly, or I'm somehow not passing the categoryId and userId correctly so that when it finally gets to handleCatClick(categoryId, categoryUserId) in operations.js, it ends up as undefined. It's probably something simple but I can't spot it. NOTE: I haven't included files like the types.js or reducers.js, because they seem to be outside the scope of the problem, but if you need them please let me know. Thanks in advance for your help!
Try this changes: Add params to these handlers
const handleCatClick = (categoryId, categoryUserId) => dispatch(categoriesOperations.handleCatClick(categoryId, categoryUserId));
and
return <Category className="category" handleClick={(categoryId, categoryUserId) => this.props.handleCatClick(categoryId, categoryUserId)} key={shortid.generate()} categoryData={item} />
Related
import { useSelector, useDispatch } from "react-redux";
import { useEffect, useState } from "react";
import request from "../../requests";
import { fetchMovies } from "../../feautures/movies/moviesSlice";
import "./SingleMoviePage.scss";
import Rating from "../../components/UI/Rating/Rating";
import axios from "axios";
const SingleMoviePage = ({ match }) => {
const dispatch = useDispatch();
const [movieDetails, setMovieDetails] = useState({})
/* params */
const movieId = match.params.id;
const page = match.params.page;
const genre = match.params.genre;
/* movies reducer handle */
const movies = useSelector((state) => state.movies.movies);
const moviesStatus = useSelector((state) => state.movies.status);
/* movieDetails reducer handle */
/* base urls */
const baseImgUrl = "https://image.tmdb.org/t/p/original";
const movieDetailUrl = `https://api.themoviedb.org/3/movie/${movieId}?api_key=c057c067b76238e7a64d3ba8de37076e&language=en-US`;
useEffect(() => {
const fetchData = async() => {
let response = await axios.get(movieDetailUrl);
response = response.data;
setMovieDetails(response)
}
fetchData()
},[movieDetailUrl])
console.log("data: ",movieDetails )
let content;
if (moviesStatus === "loading") {
<div>Loading ...</div>;
} else if (moviesStatus === "succeeced") {
let movie = movies.find((movie) => movie.id.toString() === movieId);
content = (
<div
className="single-movie__container"
style={{
backgroundImage: `url(${
movie.backdrop_path
? baseImgUrl + movie.backdrop_path
: baseImgUrl + movie.poster_path
})`,
}}
>
<div className="single-movie__information">
<h1 className="single-movie__title">{movie.title}</h1>
<div className="single-movie__rate">
<Rating
rating={movie.vote_average}
className="single-movie__stars"
/>
<div className="single-movie__average">
{movie.vote_average}(Imdb)
</div>
</div>
<p className="single-movie__overview">{movie.overview}</p>
<p className="single-movie__genres">
<label>Genres</label>
{
movieDetails.genres.map(genre => {
console.log("genre: ",genre)
return(
<div>{genre.name}</div>
)
})
}
</p>
</div>
</div>
);
}
useEffect(() => {
if (genre === "POPULAR") {
dispatch(fetchMovies(request.fetchPopular(page)));
} else if (genre === "NOW PLAYING") {
dispatch(fetchMovies(request.fetchNowPlaying(page)));
} else if (genre === "UP COMING") {
dispatch(fetchMovies(request.fetchUpComing(page)));
}
}, [dispatch, genre, page]);
return <div className="single-movie">{content}</div>;
};
export default SingleMoviePage;
I'm trying to make a movie website with react-redux. The issue is when I try to get movie details using useEffect and try to map that in:
<p className="single-movie__genres">
I get TypeError: Cannot read property 'map' of undefined error and I get empty object (data: {}) using console.log("data: ", movieDetails).
But if I refresh the page everything works well and I get
data:
{
adult: false,
backdrop_path: "/6MKr3KgOLmzOP6MSuZERO41Lpkt.jpg",
...
}
using console.log("data: ", movieDetails). Why can't I get data when the page is first loaded?
It is because your initial state does not contain "genres" array inside the object. And when react tries to handle
movieDetails.genres.map(...)
it fall down because movieDetails.genres is undefined (and undefined does not support map method of course). Either include empty array in you initial state like:
const [movieDetails, setMovieDetails] = useState({genres:[]})
or use "?" operator in your chain like:
movieDetails.genres?.map(...)
.map method is a prototype function for type array. you should declare moviedetails as an array like this when setting the default value using useState hook.
const [movieDetails, setMovieDetails] = useState([])
There is a point that i dont understand.As far as i know when the component is first loaded first useEffect worked and filled my movieDetails with datas after that map func worked.I mean js works top to bottom and
shouldn't movieStatus be filled with data until it comes to the map function?
so I've got an really simple react-app. It renders some cars on the first render, when you click details, it takes you to another router and shows only that vehicle based on its ID.
It's all okay when you follow the right order, open up the page, redux gets filled with data, car cards render up, then you click 'details' button, react-router steps in and routes us to some particular car's id, based on the ID we see the car.
BUT... at that point, when you try to click re-render the page, I get nothing from my redux store, what am I need to do? Do I need to inplement in my every component that needs redux store to fetch items if there's not?
This is my slice from redux
import { createSlice, createEntityAdapter, createAsyncThunk } from '#reduxjs/toolkit'
import axios from 'axios'
const carAdapter = createEntityAdapter();
// async action
export const fetchCars = createAsyncThunk('cars', async () =>{
const car = await axios.get('http://localhost:5000/api/cars');
return car.data.data
});
const carSlice = createSlice({
name: 'cars',
initialState: carAdapter.getInitialState({
status: 'idle',
error: null
}),
reducers:{
},
extraReducers :{
[fetchCars.pending]: (state, action) => {
state.status = 'loading'
},
[fetchCars.fulfilled]: (state, action) => {
state.status = 'fulfilled'
carAdapter.setAll(state, action.payload)
},
[fetchCars.rejected]: (state, action) => {
state.status = 'failed'
state.error = action.error.message
}
}
})
export const {
selectAll: selectAllCars,
selectById : selectCarById,
} = carAdapter.getSelectors(state => state.cars)
export default carSlice.reducer
This is my first page, where I render all the vehicles from my api
import React, { useEffect } from 'react'
import { Link } from 'react-router-dom'
import { useDispatch, useSelector } from 'react-redux'
import { fetchCars, selectAllCars } from '../features/car/carSlice'
import './car.css'
export default function Car() {
const dispatch = useDispatch();
const carStatus = useSelector(state => state.cars.status)
const cars = useSelector(selectAllCars)
useEffect(() => {
if(carStatus === 'idle') {
dispatch(fetchCars());
}
}, [dispatch, carStatus])
return (
<>
{
cars.map(car => {
return (
<div key={car.id} className="card">
<img src={car.vehiclePhoto} alt="vehicle" className="vehicle-img" />
<div className="card-container">
<h4>{car.vehicleName}</h4>
<p>{car.price}</p>
<Link to={car.id}>Details</Link>
</div>
</div>
)
})
}
</>
)
}
This is where the issue begins when you try to re-load the page
export default function Car({ match: { params: { id } } }) {
const state = useSelector(state => state)
const car = selectCarById(state, id);
return (
<div className="card">
{ car ?
<>
<img src={car.vehiclePhoto} alt="vehicle" class="vehicle-img" />
<div className="card-container">
<h4>{car.vehicleName}</h4>
<p>{car.price}</p>
</div>
</> : 'loading...'
}
</div>
)
}
Every page of the app needs to be able to load its own data. On a page which displays details for a single car, you want it to look in the state, select the data if it's already loaded, and dispatch a request for the data if it hasn't been loaded (like when you go to that page directly).
You'll want to use a different API endpoint on the single car page than the one that you use on the home page because you want to load a single car's details from the id. It's probably something like 'http://localhost:5000/api/cars/123' for id #123.
Looking at the last image, I think line 9 should be
return selectCarById(state, id)
I am trying to map over an API that is set up like the following picture:
Currently my code is set up so that I can call the headline, source, and image of which ever number I choose. I.e. if i do
const headline = res.data[0]['headline'];
this.setState({ headline })
I can get the first headline, under the name '0'
const headline = res.data[1]['headline'];
this.setState({ headline })
Then I can get the second headline, under the name '1'.
But rather than writing the same code 5-10 times, I am trying map through it so that I can return each version sliced to how many I want. I do not know how to setup the syntax, as I thought I would do something like:
<h1>
{
res.data.length && res.data.map(data => (
{data.headline}
))
}
</h1>
I got res.data is not defined as I did this in the return and not under getStock, not sure how to set this up.
News.js
import React, { Component } from 'react'
import axios from "axios"
export class News extends React.Component {
constructor(props) {
super(props);
this.state = {
headline: "",
source: "",
image: ""
}
}
componentDidMount() {
this.getStock();
}
getStock() {
const API_KEY = '*********************';
const API_CALL = `https://cloud.iexapis.com/stable/stock/aapl/news?token=${API_KEY}`;
axios.get(API_CALL)
.then(res => {
const headline = res.data[1]['headline'];
this.setState({ headline })
const source = res.data[1]['source'];
this.setState({ source })
const image = res.data[1]['image'];
this.setState({ image })
})
}
render() {
return (
<div>
<h1>{this.state.headline} </h1>
<p>{this.state.source}</p>
<img src={this.state.image} alt="img" />
</div>
)
}
}
export default News
You fist approach using .map was right you just need to make sure to check if res exists.
res && res.data.length && res.data.map(data => ...
I have a component that displays a list of movie genres generated from a Movies API. What I want is that when the user clicks on the specific genre it displays the movies from that specified genre.
My problem is that the only way I can think of doing this is to literally make a different component for each genre, make a different action creator for each genre in my Redux that does a GET request to the API for each genre, and set the Link to that component for the specified genre. That seems really time-consuming and inefficient.
Is there a way I can make the ShowGenres component display different movies depending on what genre the user clicks on in the Genre component or is the solution I thought of the only way?
Here's my Redux:
import {createStore, applyMiddleware} from "redux";
import axios from "axios";
import thunk from "redux-thunk";
export const displayGenres = () => {
return dispatch => {
axios.get("https://api.themoviedb.org/3/genre/movie/list?api_key=<api-key>&language=en-US").then(response => {
dispatch({
type: "DISPLAY_GENRES",
genres: response.data.genres
})
}).catch(err => {
console.log(err);
})
}
}
export const selectedGenre = id => {
return dispatch => {
axios.get(`https://api.themoviedb.org/3/discover/movie?api_key=<api-key>&language=en-US&include_adult=false&include_video=false&page=1&primary_release_year=2017&with_genres=9648`).then(response => {
dispatch({
type:"SELECTED_GENRE",
select: response.data.results
})
}).catch(err => {
console.log(err);
})
}
}
const reducer = (prevState = {}, action) => {
switch(action.type){
case "DISPLAY_GENRES":
return {
genres: action.genres
}
case "SELECTED_GENRE":
return {
select: action.select,
}
default:
return prevState
}
}
const store = createStore(reducer, applyMiddleware(thunk));
export default store;
Here's my Genres component that displays all the individual Genres for the user to choose:
import React, {Component} from "react";
import {connect} from "react-redux";
import {displayGenres} from "./redux";
import {Link} from "react-router-dom";
import Navbar from "./Navbar";
class Genres extends Component {
constructor(){
super();
}
componentDidMount(){
this.props.displayGenres();
}
render(){
const mappedGenres = this.props.genres && this.props.genres.map(genre => {
return (
<div className="mappedGenres">
<Link to="/showGenres">{genre.name}</Link>
</div>
)
})
return(
<div>
<Navbar/>
<div className="genre">
{mappedGenres}
</div>
</div>
)
}
}
export default connect(state => state, {displayGenres})(Genres);
And here's my ShowGenres component where I want to display movies depending on the Genre that the user clicks on in the Genre component:
import React, {Component} from "react";
import {connect} from "react-redux";
import {selectedGenre} from "./redux";
import {displayGenres} from "./redux";
class ShowGenres extends Component {
constructor(){
super();
}
componentDidMount(){
this.props.selectedGenre(this.mappedId);
}
render(){
const mappedId = this.props.genres && this.props.genres.map(id => {
return id.id;
})
const mappedSelected = this.props.select && this.props.select.map(genre => {
return (
<div>
<h1>{genre.title}</h1>
</div>
)
})
return(
<div>
{mappedSelected}
</div>
)
}
}
export default connect(state=> state, {displayGenres, selectedGenre})(ShowGenres);
One way you can do this is to have a method that takes input and sets the state with said input. Now where you go with it from here can vary.
You can have the second component nested inside the first, pass the method as a prop, and for each movie, add an on click event listener that fires the method from props, passing the genre of the movie you clicked as a parameter. The method has to be binded to the parent before being passed down to a child
Will write up an example and post shortly.
Here's an example. There's a minor bug somewhere, but the general idea is there. I've used it before but it's been almost a year since touching react. Forgive the sloppiness
const AllMovies = {
Horror : ['Tax Season', 'Family Dinner', 'DMV Trip 3'],
Comedy : ['Pasion of the Christ', 'The Earth is Flat'],
Romance : ['Me, Myself and Bacon', 'There\'s Something About Ice Cream']
}
class Movie
{
constructor(title, genre)
{
this.genre = genre;
this.title = title;
}
}
class ChildContainer extends React.Component
{
constructor(props)
{
super(props);
}
GenerateList()
{
let output = [];
for (var genre in AllMovies)
{
let movies = AllMovies[genre];
for (let i = 0; i < movies.length; i++)
{
output.push(new Movie(movies[i], genre))
}
}
return output.map(movie => {
return(
<li onClick={() => this.props.CallParent(movie.genre)}>
{movie}
</li>
)
})
}
render()
{
return(
<ul>
{this.GenerateList()}
</ul>
)
}
}
class ParentContainer extends React.Component
{
constructor(props)
{
super(props);
this.state = {
CurrentValue : []
}
this.PropMethod = this.PropMethod.bind(this);
}
PropMethod(newValue)
{
console.log('__GENRE__ : ', newValue);
let genre = AllMovies[newValue]
this.setState({
CurrentValue : genre
})
}
render() {
let genres = this.state.CurrentValue.map(val => {
return(<li>{val}</li>)
})
return(
<section>
<ul>
{genres}
</ul>
<ChildContainer
CallParent={this.PropMethod}
/>
</section>
)
}
}
class App extends React.Component
{
constructor(props)
{
super(props);
}
render() {
return(
<section>
<ParentContainer/>
</section>
)
}
}
React.render(<App />, document.getElementById('root'));
UPDATE
It should work now :D
There's another way. Remember that all of these components are classes. You can create an instance of both and pass one function to the others props as well. This will allow you to do it without nesting them in eachother.
Thank you for stopping by to help. I am working with a react/redux app. One of the component is using a lifecyle method to retrieve data from an API. Once recieved, the data JSON data is held within an array. My initialState for the data coming back is an empty array.
When the component listening to the state change is mounted, the data is rendered on to the page, but then 2 seconds later I am getting a
Uncaught TypeError: jobs.map is not a function
Component making the API call using lifecyle method and listening for state change
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getJobs } from '../../actions';
import { Card, Grid, Image, Feed } from 'semantic-ui-react';
// import './home.css';
const renderJobs = jobs => jobs.map((job, i) => (
<Card.Group stackable key={i}>
<Card className="jobscard">
<Card.Content>
<Card.Header href={job.detailUrl} target="_blank">{job.jobTitle}</Card.Header>
<Card.Meta>{job.location}</Card.Meta>
<Card.Description>{job.company}</Card.Description>
</Card.Content>
</Card>
</Card.Group>
));
class GetJobs extends Component {
componentDidMount() {
this.props.getJobs();
}
render() {
const { jobs } = this.props;
return (
<div className="getjobs">
{renderJobs(jobs)}
</div>
);
}
}
export default connect(({ jobs }) => ({ jobs }), { getJobs })(GetJobs);
Action creator/action
export const getJobsRequest = () => fetch('https://shielded-brushlands-43810.herokuapp.com/jobs',
)
.then(res => res.json());
// action creator
export const getJobs = () => ({
type: 'GET_JOBS',
payload: getJobsRequest(),
});
Reducer
import initialState from './initialState';
export default function (jobs = initialState.jobs, action) {
switch (action.type) {
case 'GET_JOBS_PENDING':
return { ...jobs, isFetching: true };
case 'GET_JOBS_FULFILLED':
return action.payload;
case 'GET_JOBS_REJECTED':
return jobs;
default:
return jobs;
}
}
And intial state
export default {
userData: {},
jobs: [],
}
enter image description here
any thoughts on why this is happening?
You can put a simple check to ensure that your jobs is ready before you attempt rendering it.
{jobs.length && renderJobs(jobs)}