I have a list of comments on soccer champions, and am trying to display comments of each soccer champion separately. I'm trying to order by id in firebase, but don't know how I can pass id from the champion component to the component where I display all the comments. It's just giving me undefined for some reason. Any help is greatly appreciated!
champ.js
import React, { Component } from "react";
import { ChampsRef, timeRef } from "./reference";
import { getsinglechamp } from "../actions/champs";
import { connect } from "react-redux"; // this is not being used. oh isee so like this?
import { Redirect, Link } from "react-router-dom";
import { Container, Row, Col } from "reactstrap";
import { Upvote } from "react-upvote";
import Form from "./Form";
import { Icon } from "react-icons-kit";
import Specials from "./specials";
import app from "../config/dev";
import { chevronDown } from "react-icons-kit/fa/chevronDown";
import { chevronUp } from "react-icons-kit/fa/chevronUp";
class OneChamp extends Component {
state = {
name: "",
weak: [],
img: "",
authenticated: false,
currentUser: null,
email: ""
};
componentDidMount() {
app.auth().onAuthStateChanged(user => {
if (user) {
this.setState({
currentUser: user,
email: user.email,
authenticated: true
});
} else {
this.setState({
currentUser: null,
authenticated: false
});
}
});
}
componentWillMount() {
const { dispatch, match } = this.props;
dispatch(getsinglechamp(match.params.id));
console.log(this.props);
}
render() {
console.log(this.props.champ);
const { dispatch, loading } = this.props;
const authenticated = this.state.authenticated;
console.log("change", this.props);
console.log("c", this.props.champ.img);
console.log("", this.props.champ.stats);
const champ = this.props.champ.stats;
let content = null;
if (loading) {
content = <p>Loading...</p>;
} else {
content = (
<div id="f">
<div className="ChampionHeader_row_ArTlM">
<div
style={{
paddingRight: "0.75rem",
paddingLeft: "0.75rem",
flexGrow: 1
}}
>
<div style={{ display: "flex", marginBottom: "1.5rem" }}>
<div style={{ flexShrink: 0, marginRight: "1rem" }}>
<div className="IconChampion_component_2qg6y IconChampion_lg_2QLBf">
<img
className="v-lazy-image v-lazy-image-loaded IconChampion_img_3U2qE"
src={this.props.champ.img}
height="80px"
/>
</div>
</div>
</div>
</div>
</div>
<div className="timeline-panel">
<div className="timeline-heading">
{" "}
<h4>{this.props.champ.name}</h4>
</div>
<ul>
{Object.keys(champ).map((item, i) => (
<div className="card">
<li className="travelcompany-input" key={i}>
<div> {champ[item]}</div>
</li>
</div>
))}
</ul>
<br />
<div className="w3-container">
// place i want to pass id <Comments id={this.props.champ.id} />
<h2>Weak To</h2> <br />
<ul className="w3-ul w3-card-4">
<li className="w3-bar">
<img
src={this.props.champ.img2}
className="w3-bar-item w3-circle w3-hide-small"
style={{ width: 145 }}
/>
<div className="w3-bar-item">
<span className="w3-large">{this.props.champ.weak}</span>
<br />
<span id="item"> Mid </span>
<div className="col-sm-5">
<span className="label label-primary">
{this.props.champ.win}
</span>
</div>
</div>
</li>
<li className="w3-bar">
<img
src={this.props.champ.img3}
className="w3-bar-item w3-circle w3-hide-small"
style={{ width: 145 }}
/>
<div className="w3-bar-item">
<Link to={`/Matchup/${this.props.champ.id}`}>
{" "}
<span className="w3-large">{this.props.champ.weak3}</span>
<br />{" "}
</Link>
<span id="item"> Mid </span>
<span className="label label-primary">
{this.props.champ.win}
</span>
</div>
</li>
</ul>
</div>
</div>
<div />
{authenticated ? (
<div className="nav-item">
<Form id={this.props.champ.id} />
</div>
) : (
<div className="nav-item">
<Link to="/login" className="nav-link2">
{" "}
Login to post
</Link>
</div>
)}
</div>
);
}
return <div>{content}</div>;
}
}
const mapStateToProps = state => {
console.log("champs", state.champs);
console.log(state.loading);
return {
champ: state.champs.champ,
loading: state.loading
};
};
export default connect(
mapStateToProps,
null
)(OneChamp);
comments.js
import React, { Component } from "react";
import axios from "axios";
import app from "../config/dev";
import { Link } from "react-router-dom";
import { ChampsRef, CommentsRef, timeRef } from "./reference";
import { connect } from "react-redux";
import { getsinglechamp } from "../actions/champs";
class Comments extends Component {
state = {
comments: [],
champ_id: "",
loading: true,
email: ""
};
componentWillMount() {
const champ_id = this.props.champ.id;
console.log("id", this.props.champ);
CommentsRef.orderByChild("champ_id")
.equalTo(`${champ_id}`)
.on("value", snap => {
const tasks = [];
let comments = [];
snap.forEach(child => {
comments.push({ ...child.val(), key: child.key });
});
this.setState({ comments: comments, Loading: false });
});
}
render() {
const { dispatch, loading } = this.props;
const { comments, ChampsLoading } = this.state;
const orderedchamps = comments;
let commentList;
if (ChampsLoading) {
commentList = <div className="TaskList-empty">Loading...</div>;
} else if (comments.length) {
commentList = (
<ul className="TaskList">
{comments.map(comment => (
<div>{comment.text}</div>
))}
</ul>
);
}
return (
<div>
<h1> Popular Cham</h1>
<p> {commentList} </p>
</div>
);
}
}
const mapStateToProps = state => {
console.log("champs", state.champs);
console.log(state.loading);
return {
champ: state.champs.champ,
loading: state.loading
};
};
export default connect(
mapStateToProps,
null
)(Comments);
Related
I'm looking for information in a database, in an app with react. In the actions.js file I can see the response.data data, when I put a console.table.
But even exporting, when I try to display the information, the variable's value comes as undefined.
I've already looked at the original file I'm using to learn, and I couldn't see where the error is.
action.js
import { CLEAR_SEARCH, SEARCH_CUSTOMERS } from "./actionTypes";
import Axios from "axios";
import { SEARCH_CUSTOMERS_URL } from "../../configs";
export const searchCustomers = (lat, lng, query) => dispatch => {
Axios.post(SEARCH_CUSTOMERS_URL, {
q: query,
})
.then(response => {
const customers = response.data;
console.log( "services/searchCustomers/actions.js, line 30" );
console.log( customers ); //ok, showing data
return dispatch({ type: SEARCH_CUSTOMERS, payload: customers });
})
.catch(function(error) {
console.log(error);
});
};
export const clearSearch = () => dispatch => {
const customers = [];
return dispatch({ type: CLEAR_SEARCH, payload: customers });
};
reducer.js
import { CLEAR_SEARCH, SEARCH_CUSTOMERS } from "./actionTypes";
const initialState = {
customers: []
};
export default function(state = initialState, action) {
switch (action.type) {
case SEARCH_CUSTOMERS:
return { ...state, customers: action.payload };
case CLEAR_SEARCH:
return { ...state, customers: action.payload };
default:
return state;
}
}
actionTypes.js
export const SEARCH_CUSTOMERS = "SEARCH_CUSTOMERS";
export const CLEAR_SEARCH = "CLEAR_SEARCH";
index.js
import React, { Component } from "react";
import { clearSearch, searchCustomers } from "../../../../services/searchCustomers/actions";
import Dialog from "#material-ui/core/Dialog";
import DelayLink from "../../../helpers/delayLink";
import Ink from "react-ink";
import LazyLoad from "react-lazyload";
import { Redirect } from "react-router";
import CustomerSearch from "../../CustomerSearch";
import CustomerSearchList from "../CustomerSearchList";
import { connect } from "react-redux";
import { debounce } from "../../../helpers/debounce";
import ContentLoader from "react-content-loader";
class SelectCustomer extends Component {
state = {
open: false,
queryLengthError: false,
loading: false,
showBgImage: true,
nothingFound: false,
};
handleCustomerSearch = debounce((query) => {
// call to searchCustomers search API
if (query.length >= 3) {
this.props.searchCustomers(
JSON.parse(localStorage.getItem("userSetAddress")).lat,
JSON.parse(localStorage.getItem("userSetAddress")).lng,
query
);
console.table( this.props ); //customers is undefined
this.setState({
queryLengthError: false,
loading: true,
nothingFound: false,
});
} else {
this.setState({ queryLengthError: true });
}
}, 400);
componentDidMount() {
//others
}
componentWillUnmount() {
//others
}
componentWillReceiveProps(nextProps) {
//this.props.customers is undefined
if (this.props.customers !== nextProps.customers) {
this.setState({ loading: false });
}
// console.log(nextProps.customers.length);
if (nextProps.customers) {
if (nextProps.customers.length === 0 ) {
this.setState({ showBgImage: true, nothingFound: true });
} else {
this.setState({ showBgImage: false, nothingFound: false });
}
}
//Overlay/Dialog.
if (nextProps.confirmSelectCustomerOpen === false) {
this.setState({ open: false });
}
if (nextProps.confirmSelectCustomerOpen === true) {
this.setState({ open: true });
}
}
//Overlay/Dialog
handleClose = () => {
this.setState({ open: false });
};
render() {
return (
<React.Fragment>
<Dialog
fullWidth={true}
fullScreen={false}
open={this.state.open}
onClose={this.handleClose}
style={{ width: "100%", margin: "auto" }}
PaperProps={{ style: { backgroundColor: "#fff", borderRadius: "10px" } }}
>
{this.state.queryLengthError && (
<div className="auth-error" style={{ marginBottom: "4rem" }}>
<div className="">{localStorage.getItem("searchAtleastThreeCharsMsg")}</div>
</div>
)}
<CustomerSearch searchFunction={this.handleCustomerSearch} />
{this.state.loading && (
{/*loading*/}
)}
{/* Here Should list customers */}
{this.props.customers && this.props.customers.length > 0 && (
<CustomerSearchList customers={this.props.customers} />
)}
{this.state.showBgImage && (
<div className="d-flex justify-content-center mt-100">
<img
className="img-fluid explore-bg"
src="/assets/img/various/explore-bg.png"
alt={localStorage.getItem("restaurantSearchPlaceholder")}
/>
</div>
)}
{this.state.nothingFound && (
<div className="auth-error" style={{ marginBottom: "4rem" }}>
<div className="error-shake">{localStorage.getItem("exploreNoResults")}</div>
</div>
)}
</Dialog>
</React.Fragment>
);
}
}
const mapStateToProps = (state) => ({
customers: state.customers
});
export default connect(
mapStateToProps,
{ searchCustomers, clearSearch }
)(SelectCustomer);
CustomerSearch/index.js
import React, { Component } from "react";
import Ink from "react-ink";
class CustomerSearch extends Component {
state = {
customer: ""
};
componentDidMount() {
this.searchInput.focus();
}
static contextTypes = {
router: () => null
};
handleInputChange = e => {
this.setState({ customer: e.target.value });
this.props.searchFunction(e.target.value);
};
render() {
return (
<React.Fragment>
<div className="col-12 p-0">
<div className="block m-0">
<div className="block-content p-0">
<div className="input-group search-box">
<div className="input-group-prepend">
<button
type="button"
className="btn search-navs-btns"
style={{ position: "relative" }}
onClick={this.context.router.history.goBack}
>
<i className="si si-arrow-left" />
<Ink duration="500" />
</button>
</div>
{/*
placeholder do input
{localStorage.getItem("restaurantSearchPlaceholder")}
*/}
<input
type="text"
className="form-control search-input"
placeholder="Pesquisar Cliente"
value={this.state.customer}
onChange={this.handleInputChange}
ref={input => {
this.searchInput = input;
}}
/>
<div className="input-group-append">
<button type="submit" className="btn search-navs-btns" style={{ position: "relative" }}>
<i className="si si-magnifier" />
<Ink duration="500" />
</button>
</div>
</div>
</div>
</div>
</div>
</React.Fragment>
);
}
}
export default CustomerSearch;
CustomerSearchList/index.js
import React, { Component } from "react";
import DelayLink from "../../../helpers/delayLink";
import Ink from "react-ink";
import LazyLoad from "react-lazyload";
class CustomerSearchList extends Component {
render() {
const { customers } = this.props;
return (
<React.Fragment>
{customers.map((customer) => (
<div key={customer.id} className="col-xs-12">
<DelayLink
to={"../stores/" + customer.id}
delay={200}
className="block block-link-shadow text-center light-bottom-border"
>
<Ink duration="500" />
</DelayLink>
</div>
))}
</React.Fragment>
);
}
}
export default CustomerSearchList;
reducers.js
import { combineReducers } from "redux";
....
....
import customerSearchReducer from "./searchCustomers/reducer";
import addressesReducer from "./addresses/reducer";
....
....
export default combineReducers({
...
customers: customerSearchReducer,
addresses: addressesReducer,
...
});
SEARCH_CUSTOMERS_URL - URL to php, get data:
$response = ['customers' => $customers];
Output customers: Array(3)
0: {id: 2, name: "Customer 1", email: "customer1#gmail.com"}
1: {id: 3, name: "Customer 2", email: "customer2#gmail.com"}
2: {id: 4, name: "Customer 3", email: "customer3#gmail.com"}
It's 3 days racking my brain. I did some research but I couldn't solve the problem. I'm new to react/reduce and was trying to figure it out based on this script.
As per comments... add searchCustomer reducer to rootReducer.
js
I have two buttons in parent component e.g button1 ,button2 ,and
I have two radio button in my child component e.g radio1,radio2 ,and both have class to active yellow
the task i need to do is onclick button1 in the parent component, the radio1 become active class and same on click on button2, radio2 in child become active class
import React ,{Component} from 'react';
import { withStyles } from '#material-ui/core/styles';
import { Radio } from '#material-ui/core';
import "react-datepicker/dist/react-datepicker.css";
import { connect } from 'react-redux';
import "./strip.css";
import * as actions from '../../actions/instruction';
import PaypalPayment from "./PaypalPayment"
import BankPayment from "./BankPayment"
class WidthdrawPayment extends Component {
state={
open: false,
cardId: 0,
}
setButton = (id) => {
this.setState({ cardId: id });
}
handlesChange = ({ id }) => {
//const { cardId } = this.props;
this.setState({ cardId: id });
};
render() {
const { open,cardId } = this.state;
return (
<>
<div className="main">
<div className="container-fluid no-padding">
<div style={{display: 'flex'}} >
<div
className=" lawyer-payment-big-box row">
<div className="col-md-12">
<div
className={cardId === 1 ? "active-div widthdraw-payment-choose-box col-sm-4 col-md-4 col-lg-4" :"widthdraw-payment-choose-box col-sm-4 col-lg-4 col-md-4"}>
<span>
<Radio
style={{color:'#fff'}}
color="primary"
checked={cardId === 1}
onClick={() => this.setButton(1)}
onChange={() => this.handlesChange({ id: 1 })}
name="radio-button-demo"
/>
</span>
<span className="widthdraw-payment-type-font" >Transfer</span>
</div>
<div
className={cardId === 2 ? "active-div widthdraw-payment-choose-box col-sm-4 col-md-4 col-lg-4" :"widthdraw-payment-choose-box col-sm-4 col-md-4 col-lg-4"}>
<span>
<Radio
style={{color:'#fff'}}
className=""
color="primary"
checked={cardId === 2}
onClick={() => this.setButton(2)}
onChange={()=> this.handlesChange({ id: 2 })}
name="radio-button-demo"
/>
</span>
<span className="widthdraw-payment-type-font">Bank Transfer</span>
</div>
<div className="col-md-4"></div>
</div>
{cardId === 0 ?
<div className="row">
<div style={{padding:'30px'}} className="col-md-12">
<p>Please select your payment method first to proceed</p>
</div>
</div>
:
<>
{
cardId ===1 ?
<>
{cardId ===1 &&
<PaypalPayment
{...this.props}
{...this.state}
/>
}
</>
:
<>
{/* <BankPayment
{...this.props}
{...this.state}
/> */}
{cardId ===2 ?
<>
{cardId===2 &&<BankPayment
{...this.props}
{...this.state}
/>
}
</>
:
<>
<div className="row">
<div style={{padding:'30px'}} className="col-md-12">
<p>Please select your payment method first to proceed</p>
</div>
</div>
</>
}
</>
}
</>
}
</div>
</div>
</div>
</div>
</div>
</>
)
}
}
export default WidthdrawPayment
import React from 'react';
import { withStyles } from '#material-ui/core/styles';
import { KeyboardDatePicker, MuiPickersUtilsProvider } from "#material-ui/pickers";
import DateFnsUtils from '#date-io/date-fns';
import {Link,Redirect} from 'react-router-dom';
import { Button, TYPES } from "../../components/atoms/YellowButton";
import { connect } from 'react-redux';
import moment from 'moment';
import * as actions from '../../actions/instruction';
import { EmptyList } from '../../components/molecules/NotFound/EmptyView';
import WidthdrawPayment from '../WidthdrawPayment'
const drawerWidth = 550;
class FirmPaymentStats extends React.Component {
constructor(props) {
super(props);
this.state={
open: false,
buttonId: 1,
cardId: 0,
}
this.superheroElement = React.createRef();
}
handlePaypalPayment = () => {
const {history} = this.props;
this.defaultactivepaypal()
history.push(`/main/dashboardmaster/widthdrawpayment`)
}
handleBankPayment = () => {
const {history} = this.props;
this.defaultActivebank()
history.push(`/main/dashboardmaster/widthdrawpayment`)
}
defaultactivepaypal = () => {
const {cardId} = this.props;
this.setState({ cardId: 1 });
}
defaultActivebank = () => {
const {cardId} = this.props;
this.setState({ cardId: 2 });
}
render() {
const { history,cardId } = this.props;
const { open,cardId } = this.state;
return (
<div className='main'>
<div className='container-fluid no-padding'>
<div className={classes.root}>
<div className="firm-payment-history-box row">
<div className='admin-options-section cases-section row'>
{/* balance bar */}
<div style={{ padding: '0px 30px' }} className="row">
<div class="lawyer-payment-subheading">
<div >
<p className="system-ui-bold-text firm-payment-main-heading" >Your Balance</p>
</div>
<div style={{paddingTop:'25px'}} className="lawyer-payment-subheading-btn-end">
<div className="system-ui-bold-text" style={{fontSize: "18px",padding: '8px 5px 0px 5px' }}>Widthdraw Balance:</div>
<button
className="yellow-button "
// checked={cardId === 1}
style={{display:'inline',marginRight: '10px'}}
onClick={this.handlePaypalPayment}
>
Paypal Account
</button>
<button
// checked={cardId === 2}
onClick={this.handleBankPayment}
className="yellow-button " style={{display:'inline'}}>
<img style={{marginRight:'10px'}}
src={require("../../assets/img/bank.png")} />
Bank Account
</button>
</div>
</div>
<div className='user-con-filter page-extras'>
{/* search bar and calender*/}
{/* table */}
</div>
<div className='pagination-dim'>
</div>
</div>
</div>
</div>
<div
style={{display: 'none'}}
>
<WidthdrawPayment
{...this.state}
{...this.props}
handlePaypalPayment={this.handlePaypalPayment}
handleBankPayment={this.handleBankPayment}
handleChange={ this.handlepaymenttypeChange}
/>
</div>
</div>
);
}
}
const wrappedFirmPaymentStats = withStyles(styles, { withTheme: true })(FirmPaymentStats);
const mapStateToProps = ({ instruction }) => ({
...instruction
});
export default connect(mapStateToProps, actions)(wrappedFirmPaymentStats)
I'm making a simple shopping cart app using Redux. Right now every time I change page, actions are automatically get called for three times which is equal to the number of items. If I go to Cart page, removeItems action gets called three times so there's no way I can add items to cart so far. It might be a problem about router, but I can't spot the problem. Could anyone explain me what is the problem?
Home.js
import React from 'react';
import { connect } from 'react-redux';
import { addToCart } from '../actions';
class Home extends React.Component {
handleClick = id => {
this.props.addToCart(id)
}
renderList = () => {
return this.props.cart.slice(0, 3).map(item => {
return (
<div className="card" key={item.id} style={{width: "200px", float: "left", marginRight: "20px"}}>
<div className="card-image">
<img src={item.imageUrl} alt={item.name} />
<span className="card-title">{item.name}</span>
<span to="/"
className="btn-floating halfway-fab waves-effect waves-light red"
onClick={this.handleClick(item.id)}
>
<i className="material-icons">add</i>
</span>
</div>
<div className="card-content">
<p>{item.desc}</p>
<p><b>${item.price}</b></p>
</div>
</div>
)
})
}
render() {
console.log(this.props.cart)
return (
<div className="container">
<h3>Home</h3>
<div className="box">
{this.renderList()}
</div>
</div>
)
}
}
const mapStateToProps = state => {
return { cart: state.cart.items }
}
const mapStateToDispatch = dispatch => {
return {
addToCart: (id) => { dispatch(addToCart(id)) }
}
}
export default connect(mapStateToProps, mapStateToDispatch)(Home);
Cart.js
import React from 'react';
import { connect } from 'react-redux';
import { removeItem } from '../actions';
class Cart extends React.Component {
handleClick = (id) => {
this.props.removeItem(id);
}
renderList = () => {
if (this.props.addedItems.length !== 0) {
return this.props.addedItems.map(item => {
return (
<li className="collection-item avatar" key={item.id}>
<div className="item-img">
<img src={item.imageUrl} alt={item.name} style={{width: "120px"}} />
</div>
<div className="item-desc">
<span className="title">{item.name}</span>
<p>{item.content}</p>
<p><b>${item.price}</b></p>
</div>
<button
className="waves-effect waves-light btn pink remove"
onClick={this.handleClick(item.id)}
>Remove</button>
</li>
)
})
}
else {
return <p>Nothing is in cart.</p>
}
}
render() {
return (
<div className="container">
<div className="cart">
<ul className="collection">
{this.renderList()}
</ul>
</div>
</div>
)
}
}
const mapStateToProps = state => {
return { addedItems: state.cart.addedItems }
}
const mapDispatchToProps = dispatch => {
return {
removeItem: (id) => {dispatch(removeItem(id))}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Cart);
Header.js
import React from 'react';
import { Link } from 'react-router-dom';
const Header = () => {
return (
<nav className="nav-wrapper">
<div className="container">
<Link to="/" className="brand-logo">Shopping</Link>
<ul className="right">
<li><Link to="/">Shop</Link></li>
<li><Link to="/cart">Cart</Link></li>
<li><Link to="/cart"><i className="material-icons">shopping_cart</i></Link></li>
</ul>
</div>
</nav>
)
}
export default Header;
Reducers
import data from '../data.json';
import { ADD_TO_CART, REMOVE_FROM_CART } from "../actions/types";
const INITIAL_DATA = {
items: data,
addedItems: [],
total: 0
}
const cartReducer = (state = INITIAL_DATA, action) => {
switch(action.type) {
case ADD_TO_CART:
let addedItem = state.items.find(item => item.id === action.id);
let existedItem = state.addedItems.find(item => action.id ===item.id);
if (existedItem) {
addedItem.quantity += 1;
return {
...state,
total: state.total + addedItem.price
}
}
else {
addedItem.quantity = 1;
let newTotal = state.total + addedItem.price;
return {
...state,
addedItems: [...state.addedItems, addedItem],
total: newTotal
}
}
case REMOVE_FROM_CART:
let itemToRemove = state.addedItems.find(item => action.id === item.id);
let newItems = state.addedItems.filter(item => action.id !== item.id);
let newTotal = state.total - itemToRemove.price;
return {
...state,
addedItems: newItems,
total: newTotal
}
default:
return state;
}
}
export default cartReducer;
Actions
import { ADD_TO_CART, REMOVE_FROM_CART } from "./types";
export const addToCart = (id) => {
return {
type: ADD_TO_CART,
id
}
}
export const removeItem = (id) => {
return {
type: REMOVE_FROM_CART,
id
}
}
App.js
import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Header from './Header';
import Home from './Home';
import Cart from './Cart';
const App = () => {
return (
<BrowserRouter>
<div className="app">
<Header />
<Switch>
<Route exact path="/" component={Home} />
<Route path="/cart" component={Cart} />
</Switch>
</div>
</BrowserRouter>
)
}
export default App;
Seems like you're actually calling your click handlers whenever you render your components, instead of just passing the handler function, so that's why actions are being triggered multiple times.
For instance, in your Home.js component change the code below from:
<span to="/" className="btn-floating halfway-fab waves-effect waves-light red" onClick={this.handleClick(item.id)} >
to:
<span to="/" className="btn-floating halfway-fab waves-effect waves-light red" onClick={() => { this.handleClick(item.id); }} >
And the same thing on Cart.js, change from:
<button className="waves-effect waves-light btn pink remove" onClick={this.handleClick(item.id)}>Remove</button>
to:
<button className="waves-effect waves-light btn pink remove" onClick={() => {this.handleClick(item.id); }}>Remove</button>
I have implemented a component that shows courses. Within the same component, there is functionality for filtering courses when a user searches for a certain course.
The problem I am having is when I dispatch an action for filtering courses, the redux store shows the courses state has been updated to contain only courses that match the search query but the component does not reflect the new state(i.e still shows all courses). How can I resolve this issue?
Here is what I have implemented so far
Action
export const retrieveAllCourses = (url, query) => dispatch => {
return axios
.get(url + `?title=${query}`, {
headers: myHeaders
})
.then(res => {
const fetchall = {
type: ACTION_TYPE.VIEW_COURSES,
payload: res.data.courses
};
dispatch(fetchall);
})
.catch(error => {
toast.error(error, { autoClose: 3500, hideProgressBar: true });
});
};
Reducer
import ACTION_TYPE from "../../actions/actionTypes";
const initialState = {
courses: [],
};
const coursesReducer = (state = initialState, action) => {
switch (action.type) {
case ACTION_TYPE.VIEW_COURSES:
return {
...state,
courses: state.courses.concat(action.payload.results),
};
default:
return state;
}
};
export default coursesReducer;
Search component
import React from "react";
import PropTypes from "prop-types";
class Search extends React.Component {
state = {
search: ""
};
handleChange = ev => {
ev.preventDefault();
const { onChange } = this.props;
const search = ev.target.value;
this.setState({ search });
if (onChange) {
onChange(search);
}
};
handleSearch = ev => {
ev.preventDefault();
const { onSearch } = this.props;
const { search } = this.state;
if (onSearch) {
onSearch(search);
}
};
render() {
const { id } = this.props;
window.onload = function() {
var input = document.getElementById("search-input");
input.addEventListener("keyup", function(event) {
if (event.keyCode === 13) {
event.preventDefault();
document.getElementById("mySearchBtn").click();
}
});
};
return (
<React.Fragment>
<form id={id}>
<div className="search-container">
<div className="input-group">
<input
type="text"
id="search-input"
className="search-input form-control"
placeholder="Search lessons"
onChange={this.handleChange}
/>
</div>
<div>
<button id="mySearchBtn" onClick={this.handleSearch} />
</div>
</div>
</form>
</React.Fragment>
);
}
}
Search.propTypes = {
onChange: PropTypes.func,
onSearch: PropTypes.func,
id: PropTypes.number
};
export default Search;
Navbar component
import React from "react";
import PropTypes from "prop-types";
import Search from "../../components/courses/Search";
export const LoggedInNavBar = ({ handleSearch, handleSearchChange}) => {
return (
<div className="row bobo-menu">
<div className="col-sm">
<div className="logo-container">
<a href={"/courses"}>
<h1 id="logo" className="hidden">
TUTORIALS
</h1>
</a>
</div>
</div>
<div className="col-sm">
<Search onSearch={handleSearch} onChange={handleSearchChange} />
</div>
<div className="col-sm">
<p id="motto">
TUTORIALS<span className="text-succes"> AND</span> LEARNING{" "}
<span className="text-succes">FOR ALL</span>
</p>
</div>
<div className="col-sm">
<div className="row row__menu__icons">
<div className="col-lg-3 col-md-3 col-sm-3">
<a
id="title"
href="/create"
className="btn btn-login "
data-mode="signin"
>
{" "}
<i className="fas fa-plus" />
</a>
</div>
<div className="col-lg-3 col-md-3 col-sm-3">
<a
id="title"
href="/create"
className="btn btn-login "
data-mode="signin"
>
<i className="far fa-user" />
</a>
</div>
<div className="col-lg-3 col-md-3 col-sm-3">
<a
id="title"
href="/me/stories"
className="btn btn-login "
data-mode="signin"
>
<i className="fas fa-book" />
</a>
</div>
<div className="col-lg-3 col-md-3 col-sm-3">
<a
id="title"
href="/create"
className="btn btn-login "
data-mode="signin"
>
<i className="fas fa-sign-out-alt" />
</a>
</div>
</div>
</div>
<br />
<br />
</div>
);
};
LoggedInNavBar.propTypes = {
handleSearch: PropTypes.func,
handleSearchChange: PropTypes.func
};
Courses component
import React, { Fragment } from "react";
import { details } from "../../routes/protectedRoutes";
import { AUTHENTICATED } from "../../utils/myHeaders";
import { ViewAllCourses } from "../../views/courses/viewSearchResults";
import { retrieveAllCourses } from "../../actions/courseActions/courseAction";
import { LoggedInNavBar } from "../navigation/LoggedIn";
import { API_URLS } from "../../appUrls";
import PropTypes from "prop-types";
import { connect } from "react-redux";
export class Courses extends React.Component {
constructor(props) {
super(props);
this.user = details(AUTHENTICATED);
this.state = {
search: ""
};
}
componentDidMount() {
this.fetchCourses();
}
fetchCourses = (searchStr = null) => {
let Url = API_URLS.FETCH_CREATE_ARTICLES;
const { search } = this.state;
const query = searchStr !== null ? searchStr : search;
this.props.dispatch(retrieveAllCourses( Url, query ));
};
handleSearch = search => {
this.setState({ search });
this.fetchCourses(search);
};
handleSearchChange = search => {
if (!search) {
this.setState({ search });
this.fetchCourses(search);
}
};
render() {
const { allCourses } = this.props;
return (
<div>
<LoggedInNavBar
handleSearchChange={this.handleSearchChange}
handleSearch={this.handleSearch}
/>
<ViewAllCourses results={allVideos} />
</div>
);
}
}
Courses.propTypes = {
dispatch: PropTypes.func.isRequired,
allCourses: PropTypes.array
};
const mapStateToProps = state => ({
allCourses: state.coursesReducer.courses
});
const mapDispatchToProps = dispatch => ({ dispatch });
export default connect(
mapStateToProps,
mapDispatchToProps
)(Courses);
API response
In your reducer, why are you concatenating it with previous state results? Your filtering will never show associated data if you're doing this.
I see no payload.results in your action dispatch. Shouldn't it be action.payload & not action.payload.results?
return {
...state,
courses: action.payload,
}
There's no state variable called videos in your reducer. You're dispatching & storing courses, so you have to listen to:
const mapStateToProps = state => ({
allCourses: state.coursesReducer.courses
});
Hope this is helpful!
I have a basic React app with a function in the App.js component to pass down as a prop for subsequent components to handle moving a book between shelves.
When I pass the function down one level, from App.js to ListBooks.js to be passed on the Books.js component which is where the user will select the new shelf and trigger the API call and state update, this works fine.
However when I then try to do the same from App.js > SearchBooks.js > Book.js it does not seem to work.
What I expect is for the updateShelf function to be called to update the book and the state.
Apologies for 'wall of code' if there's too much there, just not sure exactly where the issue is.
EDIT:
As suggested in comments, here is a CodeSandbox version:
https://codesandbox.io/s/github/richardcurteis/myreads-udacity
App.js
import React, { Component } from 'react'
import ListBooks from './ListBooks'
import SearchBooks from './SearchBooks'
import * as BooksAPI from './utils/BooksAPI'
import { Route } from 'react-router-dom'
class BooksApp extends Component {
state = {
books: []
}
componentDidMount() {
BooksAPI.getAll()
.then((books) => {
this.setState(() => ({
books
}))
})
}
updateShelf = (book, shelf) => {
this.state.books.forEach(b => {
if(b.id === book.id) {
b.shelf = shelf
this.setState((currentState) => ({
books: currentState.books
}))
BooksAPI.update(book, shelf)
}
});
}
render() {
return (
<div>
<Route exact path='/' render={() => (
<ListBooks
books={this.state.books}
onUpdateShelf={this.updateShelf}
/>
)} />
<Route exact path='/search' render={() => (
<SearchBooks
onUpdateShelf={this.updateShelf}
/>
)} />
</div>
)
}
}
export default BooksApp
SearchBooks.js
import React, { Component } from 'react'
import * as BooksAPI from './utils/BooksAPI'
import Book from './Book';
export default class SearchBooks extends Component {
state = {
query: '',
books: []
}
updateQuery(query) {
this.setState(() => ({
books: [],
query: query
}))
this.bookSearch(query)
}
bookSearch(e) {
if (e.length > 0) BooksAPI.search(e)
.then(books => this.setState(currentState => ({
books: books
})));
}
render() {
const { query, books } = this.state
const { onUpdateShelf } = this.props
return(
<div className="search-books">
<div className="search-books-bar">
<a className="close-search" >Close</a>
<div className="search-books-input-wrapper">
<input
type="text"
placeholder="Search by title, author or subject"
value={query}
onChange={(event) => this.updateQuery(event.target.value)}
/>
</div>
</div>
<div className="search-books-results">
<ol className="books-grid">
<li>
{ !books.error ? (
books.map((book) => (
<Book
key={book.id}
book={book}
updateShelf={onUpdateShelf}
/>
))
) : (
<h4>"{query}", is not a valid search</h4>
)}
</li>
</ol>
</div>
</div>
)
}
}
Book.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
class Book extends Component {
static propTypes = {
book: PropTypes.object.isRequired
}
render() {
const { book, updateShelf } = this.props
return(
<div key={book.id} className="book">
<div className="book-top">
<div className="book-cover" style={{ width: 128, height: 193, backgroundImage: `url(${book.imageLinks.thumbnail})` }}></div>
<div className="book-shelf-changer">
<select value={book.shelf ? book.shelf : 'none'} onChange={(e) => updateShelf(book, e.target.value)}>
<option disabled >Move to...</option>
<option value="currentlyReading" >Currently Reading</option>
<option value="wantToRead" >Want to Read</option>
<option value="read" >Read</option>
<option value="none" >None</option>
</select>
</div>
</div>
<div className="book-title">{book.title}</div>
<div className="book-authors">{book.authors}</div>
</div>
)
}
}
export default Book
Just for reference:
ListBooks.js
import React, { Component } from 'react';
import PropTypes from 'prop-types'
import './App.css'
import { Link } from 'react-router-dom'
import Book from './Book'
const shelves = [
{
key: 'currentlyReading',
name: 'Currently Reading'
},
{
key: 'wantToRead',
name: 'Want To Read'
},
{
key: 'read',
name: 'Read'
}
];
class ListBooks extends Component {
static propTypes = {
books: PropTypes.array.isRequired,
onUpdateShelf: PropTypes.func.isRequired
}
render() {
const { books, onUpdateShelf } = this.props
function getBooksForShelf(shelfKey) {
return books.filter(book => book.shelf === shelfKey);
}
return(
<div className="app">
<div className="list-books">
<div className="list-books-title">
<h1>My Reads</h1>
</div>
<div className="list-books-content">
<div>
{ shelves.map((shelf) => (
<div key={shelf.key} className="bookshelf">
<h2 className="bookshelf-title">{shelf.name}</h2>
{ getBooksForShelf(shelf.key).length === 0 ? (
<div>
<h4>No books in this shelf</h4>
</div>
) : (
<div className="bookshelf-books">
<ol className="books-grid">
<li>
{ getBooksForShelf(shelf.key).map((book) => (
<Book key={book.id}
book={book}
updateShelf={onUpdateShelf}/>
))}
</li>
</ol>
</div>
)}
</div>
)) }
</div>
</div>
<Link
to='/search'
className="open-search">
Find a Book
</Link>
</div>
</div>
)
}
}
export default ListBooks
As we discussed it's not a problem with the prop passing but rather with updating the state. Here is the modified code that seems to work:
updateShelf = (book, shelf) => {
const bookFromState = this.state.books.find(b => b.id === book.id);
if (bookFromState) {
// update existing
bookFromState.shelf = shelf;
this.setState(currentState => ({
books: currentState.books
}));
BooksAPI.update(book, shelf);
} else {
// add new one
this.setState(prevState => ({ books: [...prevState.books, book] }));
BooksAPI.update(book, shelf);
}
};
Or better without mutating the state:
updateShelf = (book, shelf) => {
this.setState(prevState => {
const booksCopy = prevState.books.filter(b => b.id !== book.id);
booksCopy.push({ ...book, shelf });
return { books: booksCopy }
});
BooksAPI.update(book, shelf);
};