Mapping API info to React Component - reactjs

I'm trying to create a Github search for user repos and display them in cards. My ListComponent looks good when displayed, but I'm having trouble mapping the repo info to each card. When I console.log the data it shows up as expected, but will not map to the cards. I'm using fetch for the api calls and lodash for mapping. Any help would be appreciated!
GithubSearch:
import React, { Component, Fragment } from 'react';
import ListComponent from './ListComponent';
import _ from 'lodash';
class GithubSearch extends Component {
constructor(props) {
super(props);
this.state = {
username: '',
repos: [],
apiMsg:'',
userInfo: {}
}
};
handleChange = (e) => {
this.setState({ username: e.target.value });
setTimeout(this.handleSubmit, 1000)
}
handleSubmit = () => {
fetch(`https://api.github.com/users/${this.state.username}/repos`)
.then(res => res.json())
.then(data => {
console.log(data)
})
.catch(err => {
this.setState({
repos: [],
apiMsg: err.message
})
})
}
render() {
return (
<Fragment>
<div className='header'>Github Search</div>
<form className='search'>
<input
placeholder='Github user'
name='github user'
type='text'
onChange={this.handleChange}
value={this.state.username}
/>
<button type='submit' onSubmit={this.handleSubmit}>Search</button>
</form>
<p>{this.state.apiMsg}</p>
<div className='right-container'>
{_.map(this.state.repos, repo => <ListComponent key={repo.id} {...repo}/>)}
</div>
</Fragment>
)
}
}
export default GithubSearch;
ListComponent:
import React from 'react'
import '../App.css'
const ListComponent = ({ name, description, language, html_url }) => {
return (
<div className='card'>
<div className='card-body'>
<h2>{name}</h2>
</div>
<div className='card-body'>
<p>{description}</p>
</div>
<div className='card-body'>
<p>Languages: {language}</p>
</div>
</div>
);
}
export default ListComponent;

You have forgotten to set state for repo after API response , you try below code
handleSubmit = () => {
fetch(`https://api.github.com/users/${this.state.username}/repos`)
.then(res => res.json())
.then(data => {
console.log(data)
this.setState({repo:data})
})
.catch(err => {
this.setState({
repos: [],
apiMsg: err.message
})
})
}

Related

React Api call: Displaying only 1st onSubmit but nothing after

I have two components, CryptoPrice with a coin prop which calls an API to get the price, and Nav where I search for a coin, and it renders the CryptoPrice component assigning the onSubmit value to CryptoPrice coin prop.
The display works good until I do a second onSubmit from the Nav. When I do a second onSubmit, nothing changes.
App.js code:
import CryptoPrice from "./components/CryptoPrice";
import Nav from "./components/Nav";
function App() {
return (
<div className="App">
<header className="App-header">
<h1>Crypto Prices</h1>
<div className="flex">
<CryptoPrice coin="bitcoin" />
<CryptoPrice coin="ethereum" />
</div>
<div>
<Nav></Nav>
</div>
</header>
</div>
);
}
CryptoPrice component:
import styles from "./css/CryptoPrice.module.css";
export default class CryptoPrice extends React.Component {
constructor(props) {
super(props);
this.state = {
price: [],
url: `https://api.coingecko.com/api/v3/simple/price?ids=${this.props.coin}&vs_currencies=usd`,
};
}
componentDidMount = () => {
this.loadData();
setInterval(this.loadData, 20000);
};
loadData = () => {
fetch(this.state.url)
.then((response) => response.json())
.then((data) => {
let key = Object.keys(data);
return data[key];
})
.then((coin) => {
let price = coin.usd;
this.setState({ price });
});
};
render() {
return (
<div className={styles.padding}>
<h2>{this.props.coin} price</h2>
<div>{this.state.price}$</div>
</div>
);
}
}
Nav Component
import CryptoPrice from "./CryptoPrice";
export default class Nav extends React.Component {
constructor(props) {
super(props);
this.state = {
coin: "",
isSubmitted: false,
};
}
componentDidMount() {
this.setState({ isSubmitted: false });
}
render() {
return (
<div>
<form
onSubmit={(e) => {
e.preventDefault();
this.setState({ isSubmitted: true });
}}
>
<input
type="text"
onChange={(e) => {
this.setState({ coin: e.target.value });
}}
></input>
<input type="submit" value="Add"></input>
</form>
{this.state.isSubmitted && <CryptoPrice coin={this.state.coin} />}
</div>
);
}
}
Thanks so much for any help/feedback
Your issue is because you are setting the url in state so it will not update when the props update. Try changing you fetch function to use props directly(also remember to clear the setInterval when you unmount):
loadData = () => {
fetch(`https://api.coingecko.com/api/v3/simple/price?ids=${this.props.coin}&vs_currencies=usd`)
.then((response) => response.json())
.then((data) => {
let key = Object.keys(data);
return data[key];
})
.then((coin) => {
let price = coin.usd;
this.setState({ price });
});
};

how to make child component update when form in parent component submit

Hi I've been at this for two days now and is not getting any solution or answers. It is getting on my nerves and frustrating.
What I am trying to do is to update the list in child component after I hit submit that POST to the db on my api server. The child component is print a list of all records from the DB. When I submit, the child should re-render that list all the record from DB including the one just submitted.
I had to hit page refresh to get the list updated including the newly posted record. I don't want to refresh the page. Just the list (child) component.
I tried every possible solution from Stackoverflow, Google, etc.
Im using React 16.10.
See the entire code below. Tell me what I need to change to make it work.
Im getting headache. Im going to get Tylenol after I post this questions.
Ill start with app.js:
import React, { Component } from 'react';
import { BrowserRouter as Router, Route } from "react-router-dom";
import './App.css';
import Navigation from './components/Navigation';
import TaskList from './components/tasklist';
import EditTask from './components/listEdit';
import CreateList from './components/listCreate';
class App extends Component {
render() {
return (
<Router>
<div>
<Navigation />
<div className="container">
<Route path="/" exact component={TaskList} />
<Route path="/edit/:id" component={EditTask} />
<Route path="/create" component={CreateList} />
</div>
</div>
</Router>
);
}
}
export default App;
listCreate.js (parent component)
import React, { Component } from 'react';
import TaskList from './tasklist';
import axios from 'axios';
export default class CreateList extends Component {
constructor(props) {
super(props);
this.onChangeListStatus = this.onChangeListStatus.bind(this);
this.onChangeListItem = this.onChangeListItem.bind(this);
this.onChangeListDue = this.onChangeListDue.bind(this);
this.onSubmit = this.onSubmit.bind(this);
this.state = {
list_status: '',
list_item: '',
list_due: '',
list_created: ''
}
}
onChangeListStatus(e) {
this.setState({
list_status: e.target.value
});
}
onChangeListItem(e) {
this.setState({
list_item: e.target.value
});
}
onChangeListDue(e) {
this.setState({
list_due: e.target.value
});
}
onSubmit(e) {
e.preventDefault();
console.log(`Form submitted:`);
console.log(`Item Status: ${this.state.list_status}`);
console.log(`Item: ${this.state.list_item}`);
console.log(`Item Due: ${this.state.list_due}`);
const newItem = {
list_status: this.state.list_status,
list_item: this.state.list_item,
list_due: this.state.list_due,
};
axios.post('http://localhost:4000/lists/add', newItem)
.then(res => console.log(res.data));
this.setState({
list_status: '',
list_item: '',
list_due: '',
})
}
render() {
return (
<div>
<div style={{marginTop: 10}}>
<h3>Create New Item</h3>
<form onSubmit={this.onSubmit}>
<div className="form-group">
<label>New Item: </label>
<input type="text"
className="form-control"
value={this.state.list_item}
onChange={this.onChangeListItem}
/>
</div>
<div className="form-group">
<label>Due Date: </label>
<input
type="text"
className="form-control"
value={this.state.list_due}
onChange={this.onChangeListDue}
/>
</div>
<div className="form-group">
<label>Status: </label>
<input
type="text"
className="form-control"
value={this.state.list_status}
onChange={this.onChangeListStatus}
/>
</div>
<div className="form-group">
<input type="submit" value="Create Item" className="btn btn-primary" />
</div>
</form>
</div>
<TaskList reload={"true"}/>
</div>
)
}
}
tasklist.js (child)
import React, { Component } from 'react';
import ItemRow from './itemRow';
import ItemField from './itemField';
import axios from 'axios';
export default class TaskList extends Component {
constructor(props) {
super(props);
this.state = { refreshlist: '',
lists: []
};
}
componentWillReceiveProps(nextProps) {
this.setState({ data: nextProps.data });
}
componentDidMount() {
axios.get('http://localhost:4000/lists/')
.then(response => {
this.setState({ lists: response.data });
})
.catch(function (error){
console.log(error);
})
}
// componentWillReceiveProps(props) {
// this.setState(this.state)
// }
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.total !== prevState.total) {
return (this.setState({ refreshlist: nextProps.refreshlist })) // <- this is setState equivalent
}
// etc...
}
listoftask() {
return this.state.lists.map(function(currentItem, i){
return <ItemRow list={currentItem} key={i} />;
})
}
render() {
return (
<div>
<table className="table table-striped" style={{ marginTop: 20 }} >
<thead>
<ItemField />
</thead>
<tbody>
{ this.listoftask() }
</tbody>
</table>
</div>
)
}
}
update:
This is the api server i use to send data from db
// const dotenv = require("dotenv");
import dotenv from 'dotenv';
import express from 'express';
import cors from 'cors';
// import uuidv4 from 'uuid/v4';
import mongoose from 'mongoose';
const app = express();
const listRoutes = express.Router();
dotenv.config();
const PORT = process.env.PORT || 4000;
const URI_lists = 'mongodb://localhost:27017/lists';
let List = require('./models/task');
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({
extended: true
}));
mongoose.connect(URI_lists,
{useNewUrlParser: true,
useUnifiedTopology: true},
)
.then(() => {
console.log("MongoDB database initial connection established successfully.");
})
.catch((err) => {
console.log("ERROR! Could not connect to Database!");
console.log(err);
});
const connection = mongoose.connection;
connection.on('disconnected',()=> {console.log('lost connection!')});
connection.on('reconnected',()=> {console.log('reconnected to db again!')});
listRoutes.route('/').get(function(req, res) {
List.find(function(err, lists) {
if (err) {
console.log(err);
} else {
res.json(lists);
}
});
});
listRoutes.route('/:id').get(function(req, res) {
let id = req.params.id;
List.findById(id, function(err, list) {
res.json(list);
});
});
listRoutes.route('/update/:id').post(function(req, res) {
List.findById(req.params.id, function(err, list) {
if (!list)
res.status(404).send("data is not found");
else
list.list_item = req.body.list_item;
list.list_status = req.body.list_status;
list.list_due = req.body.list_due;
list.list_created = req.body.list_created;
list.save().then(list => {
res.json('List item updated!');
})
.catch(err => {
res.status(400).send("Update not possible");
});
});
});
listRoutes.route('/add').post(function(req, res) {
let newitem = new List(req.body);
newitem.save()
.then(list => {
res.status(200).json({'list': 'list item added successfully'});
})
.catch(err => {
res.status(400).send('adding new list item failed');
});
});
app.use('/lists', listRoutes);
app.listen( PORT, () => {
console.log('Server is running on Port: ' + PORT);
});
here's my repo on GitHub:
(backend) https://github.com/zenkbaries/todoList
(frontend) https://github.com/zenkbaries/todolistapp
The Child component will re-render when updated props are passed to it. Otherwise it has no need to update.
Looking at your configuration, the Child component only has one prop, and it never changes. Also, the only time you would actually retrieve the updated data from your API is in componentDidMount(), which only triggers after the first initial mount of the component.
For your functionality to work as expected, you'll need to pass in an updated prop each time you submit the form. And upon receiving that update, make a new request to the API.
Without refactoring your code too much, we can do something like this:
In CreateList.js (Parent):
import React, { Component } from 'react';
import TaskList from './tasklist';
import axios from 'axios';
export default class CreateList extends Component {
constructor(props) {
super(props);
this.onChangeListStatus = this.onChangeListStatus.bind(this);
this.onChangeListItem = this.onChangeListItem.bind(this);
this.onChangeListDue = this.onChangeListDue.bind(this);
this.onSubmit = this.onSubmit.bind(this);
this.state = {
new_item: {},
list_status: '',
list_item: '',
list_due: '',
list_created: ''
}
}
onChangeListStatus(e) {
this.setState({
list_status: e.target.value
});
}
onChangeListItem(e) {
this.setState({
list_item: e.target.value
});
}
onChangeListDue(e) {
this.setState({
list_due: e.target.value
});
}
onSubmit(e) {
e.preventDefault();
console.log(`Form submitted:`);
console.log(`Item Status: ${this.state.list_status}`);
console.log(`Item: ${this.state.list_item}`);
console.log(`Item Due: ${this.state.list_due}`);
const newItem = {
list_status: this.state.list_status,
list_item: this.state.list_item,
list_due: this.state.list_due,
};
axios.post('http://localhost:4000/lists/add', newItem)
.then(res => {
this.setState({
list_status: '',
list_item: '',
list_due: '',
new_item: newItem
})
});
}
render() {
return (
<div>
<div style={{marginTop: 10}}>
<h3>Create New Item</h3>
<form onSubmit={this.onSubmit}>
<div className="form-group">
<label>New Item: </label>
<input type="text"
className="form-control"
value={this.state.list_item}
onChange={this.onChangeListItem}
/>
</div>
<div className="form-group">
<label>Due Date: </label>
<input
type="text"
className="form-control"
value={this.state.list_due}
onChange={this.onChangeListDue}
/>
</div>
<div className="form-group">
<label>Status: </label>
<input
type="text"
className="form-control"
value={this.state.list_status}
onChange={this.onChangeListStatus}
/>
</div>
<div className="form-group">
<input type="submit" value="Create Item" className="btn btn-primary" />
</div>
</form>
</div>
<TaskList newItem={this.state.new_item}/>
</div>
)
}
}
So we have a newItem object that gets passed to the Child. We're simply using that to identify a change.
taskList.js (Child)
import React, { Component } from 'react';
import ItemRow from './itemRow';
import ItemField from './itemField';
import axios from 'axios';
export default class TaskList extends Component {
constructor(props) {
super(props);
this.state = { refreshlist: '',
lists: []
};
}
componentDidMount() {
axios.get('http://localhost:4000/lists/')
.then(response => {
this.setState({ lists: response.data });
})
.catch(function (error){
console.log(error);
})
}
componentDidUpdate(prevProps){
if(prevProps.newItem !== this.props.newItem){
axios.get('http://localhost:4000/lists/')
.then(response => {
this.setState({ lists: response.data });
})
.catch(function (error){
console.log(error);
})
}
}
listoftask() {
return this.state.lists.map(function(currentItem, i){
return <ItemRow list={currentItem} key={i} />;
})
}
render() {
return (
<div>
<table className="table table-striped" style={{ marginTop: 20 }} >
<thead>
<ItemField />
</thead>
<tbody>
{ this.listoftask() }
</tbody>
</table>
</div>
)
}
}
In the Child component, we introduce the componentDidUpdate() hook which is triggered whenever the child component gets an updated props or state. Then we simply reapply the same logic you had in componentDidMount() to fetch the list data.

React Router/Context API Search Component

I used to make this code work out for my search component but after the on submit is called, I receive this error which never happened before, does anyone have any clue???
Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
import React, { Component } from "react";
import axios from "axios";
import { Redirect } from "react-router-dom";
import { Consumer } from "../context";
class Search extends Component {
constructor() {
super();
this.state = {
productTitle: "",
apiUrl: "*******************************",
redirect: false
};
}
findProduct = (dispatch, e) => {
e.preventDefault();
axios
.post(
`${this.state.apiUrl}`,
JSON.stringify({ query: this.state.productTitle })
)
.then(res => {
dispatch({
type: "SEARCH_TRACKS",
payload: res.data.output.items
});
this.setState({ items: res.data.output.items, redirect: true });
})
.catch(err => console.log(err));
};
onChange = e => {
this.setState({ [e.target.name]: e.target.value });
};
render() {
const { redirect } = this.state;
if (redirect) {
return <Redirect to="/searchresult" />;
}
return (
<Consumer>
{value => {
const { dispatch } = value;
return (
<div>
<form onSubmit={this.findProduct.bind(this, dispatch)}>
<div className="form-group" id="form_div">
<input
type="text"
className="form-control form-control-md"
placeholder="...محصولات دسته یا برند مورد نظرتان را انتخاب کنید"
name="productTitle"
value={this.state.productTitle}
onChange={this.onChange}
/>
<button className="btn" type="submit">
<i className="fas fa-search" />
</button>
</div>
</form>
</div>
);
}}
</Consumer>
);
}
}
import React, { Component } from 'react'
import axios from 'axios'
const Context = React.createContext();
export const axiosDashboard = () => {
const URL = (`*****************`);
return axios(URL, {
method: 'POST',
data: JSON.stringify({refresh:"true"}),
})
.then(response => response.data)
.catch(error => {
throw error;
});
};
const reducer = (state, action) => {
switch(action.type){
case 'SEARCH_TRACKS':
return {
...state,
items: action.payload,
heading: 'Search Results'
};
default:
return state;
}
}
export class Provider extends Component {
state = {
dispatch:action => this.setState(state => reducer(state, action))
}
render() {
return (
<Context.Provider value={this.state}>
{this.props.children}
</Context.Provider>
)
}
}
export const Consumer = Context.Consumer
import React, { Component } from 'react'
import { Consumer } from '../context'
import SearchResult from './SearchResult'
import './Search.css'
class Tracks extends Component {
render() {
return (
<Consumer>
{value => {
const { items } = value
if(items === undefined || items.length === 0){
return 'hello'}
else{
return(
<React.Fragment>
<div id='products_search'>
<div className='container'>
<div className="row justify-content-end">
{items.map(item => (
<SearchResult
key={item.id}
id={item.id}
title={item.name}
current_price={item.current_price}
lowest_price={item.lowest_price}
store_name={item.store_name}
thumb={item.thumb_url}/>
))}
</div>
</div>
</div>
</React.Fragment>
)
}
}}
</Consumer>
)
}
}
export default Tracks
import React from 'react'
import {Link} from 'react-router-dom'
import './Search.css'
const SearchResult = (props) => {
const {title,current_price,lowest_price,thumb,id,store_name} = props
return (
<div className="col-md-3" id="searchresult">
<img src={thumb} alt=""/>
<div className="sexy_line"></div>
<p className="muted">{store_name}</p>
<h6>{title}</h6>
<p>{lowest_price}</p>
<Link to={`products/item/${id}`}>
<button type="button" className="btn btn-light rounded-pill">{
new Intl
.NumberFormat({style: 'currency', currency: 'IRR'})
.format(current_price)
}</button>
</Link>
</div>
)
}
export default SearchResult
Maximum update depth exceeded.
This means that you are in a infinit loop of re rendering a component.
The only place where I can see this is possible to happen is in this part
if (redirect) {
return <Redirect to="/searchresult" />;
}
Maybe you are redirecing to the a route that will get the same component that have the redirect.
Please check if you aren't redirecting to the same route as this component and provide your routes and what is inside Consumer.

Where do I add a click event in reactjs

Maybe this doesen't even look like a code, but is there any way I can change other components value/state on click?
import React from 'react';
import './pokemonList.css';
import {Component} from 'react';
import Pokemon from './Pokemon';
class PokemonList extends Component {
constructor(props){
super(props);
this.state = {
pokemons : [],
pokemon : {}
};
}
componentWillMount(){
fetch('https://pokeapi.co/api/v2/pokemon/').then(res=>res.json())
.then(response=>{
this.setState({
pokemons : response.results,
});
});
}
handleClick(id) {
fetch(`https://pokeapi.co/api/v2/pokemon/${id}/`)
.then(res => res.json())
.then(data => {
const pokemon = new Pokemon(data);
this.setState({ pokemon: pokemon });
})
.catch(err => console.log(err));
console.log("click happened");
}
render(){
const {pokemons} = this.state;
return (
<div className='pokemonList'> {pokemons.map(pokemon =>(
<button onClick={this.handleClick.bind(this)} className='pokemon-
btn' key={pokemon.name}>
{pokemon.name}
</button>
))}
</div>
)
}}
export default PokemonList;
At this point I'm not even sure where does handleClick() has to be, so I put it in my App component aswell. The output is ok, but clicking these buttons doesen't seem to do anything. They are supposed to show detailed pokemon information in component.
import React, {Component} from 'react';
import './pokemon-info.css';
const PokemonInfo = ({ pokemon }) => {
const { name,
height,
weight,
sprite,
statsSpeed,
statsSpecialDefense,
statsSpecialAttack,
statsDefense,
statsAttack,
statsHp
} = pokemon;
return (
<section className="pokemonInfo">
<img src={sprite} className='sprite-image' alt="pokemon_sprite"/>
<div className='data-wrapper'>
<h3 className="data-char">{pokemon.name}</h3><br />
<p className = 'data-char'>Height: {height}</p>
<p className = 'data-char'>Weight: {weight}</p><br />
<p className = 'data-char'>Stats: </p><br />
<p className = 'data-char'>Speed: {statsSpeed}</p>
<p className = 'data-char'>Special defense: {statsSpecialDefense}</p>
<p className = 'data-char'>Special attack: {statsSpecialAttack}</p>
<p className = 'data-char'>Defense: {statsDefense}</p>
<p className = 'data-char'>Attack: {statsAttack}</p>
<p className = 'data-char'>Hp: {statsHp}</p>
</div>
</section>
)
}
export default PokemonInfo;
Here is my App component
import React, { Component } from 'react';
import './App.css';
import PokemonList from './PokemonList';
import Pokemon from './Pokemon';
import PokemonInfo from './PokemonInfo';
class App extends Component {
constructor() {
super();
this.state = {
pokemon: {}
};
this.handleOnClick = this.handleOnClick.bind(this);
}
handleOnClick(id) {
fetch(`http://pokeapi.co/api/v2/pokemon/${id}/`)
.then(res => res.json())
.then(data => {
const pokemon = new Pokemon(data);
this.setState({ pokemon });
})
.catch(err => console.log(err));
}
render() {
return (
<div className="App">
<PokemonList />
<PokemonInfo pokemon={this.state.pokemon}/>
</div>
);
}
}
export default App;
It is obvious I did go wrong somewhere, but where?
Update:
Pokemon
class Pokemon {
constructor(data) {
this.id = data.id;
this.name = data.name;
this.height = data.height;
this.weight = data.weight;
this.sprite = data.sprites.front_default;
this.statsSpeed = data.stats[0].stats.base_stat;
this.statsSpecialDefense = data.stats[1].stats.base_stat;
this.statsSpecialAttack = data.stats[2].stats.base_stat;
this.statsDefense = data.stats[3].stats.base_stat;
this.statsAttack = data.stats[4].stats.base_stat;
this.statsHp = data.stats[5].stats.base_stat;
}
}
export default Pokemon;
Your App component should keep the state and pass updater functions as props to children components:
PokemonList
import React from "react";
import "./pokemonList.css";
import { Component } from "react";
import Pokemon from "./Pokemon";
class PokemonList extends Component {
render() {
const { pokemons } = this.props;
return (
<div className="pokemonList">
{pokemons.map(pokemon => (
<button
onClick={() => this.props.handleClick(pokemon.id)} // id or whatever prop that is required for request
className="pokemon-btn"
key={pokemon.name}
>
{pokemon.name}
</button>
))}
</div>
);
}
}
PokemonInfo - no change here.
APP
import React, { Component } from "react";
import "./App.css";
import PokemonList from "./PokemonList";
import Pokemon from "./Pokemon";
import PokemonInfo from "./PokemonInfo";
class App extends Component {
constructor() {
super();
this.state = {
pokemon: {},
pokemons: [],
};
this.handleOnClick = this.handleOnClick.bind(this);
}
componentDidMount() {
fetch("https://pokeapi.co/api/v2/pokemon/")
.then(res => res.json())
.then(response => {
this.setState({
pokemons: response.results
});
});
}
handleOnClick(id) {
fetch(`http://pokeapi.co/api/v2/pokemon/${id}/`)
.then(res => res.json())
.then(data => {
const pokemon = new Pokemon(data);
this.setState({ pokemon });
})
.catch(err => console.log(err));
}
render() {
return (
<div className="App">
<PokemonList pokemons={this.state.pokemons} handleClick={this.handleOnClick} />
<PokemonInfo pokemon={this.state.pokemon} />
</div>
);
}
}
More on lifting the state up.

React renders both undefined and updated state

My code works perfectly fine when executing CRUD operations. However, I have a small issue when I was trying to edit or delete my project, which is located in ProjectEdit.js file. I could edit and delete the project and store the updated data to the MongoDB, but when I used history.push in editProject and deleteProject actions to redirect my ProjectEdit/ProjectDetail page to a Project page, the project list still shows the project that I have already edited plus an undefined key project in which I don't know how it exists. However, if I refreshed the page, there would be no problems at all. I think the problem might be Redux store that holds state tree of my application, but I am not sure. Therefore, I was wondering if you guys could help me out.
Here's an undefined project key.
My project repo is here: https://github.com/topkoong/PersonalWebsite
My staging website is the following: https://shielded-springs-57013.herokuapp.com/
Thank you for your time and consideration.
ProjectEdit.js
import _ from 'lodash';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { reduxForm, Field } from 'redux-form';
import { Link } from 'react-router-dom';
import ProjectField from './ProjectField';
import formFields from './formFields';
import * as actions from '../../actions';
import { withRouter } from "react-router-dom";
class ProjectEdit extends Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {
const { _id } = this.props.match.params;
this.props.fetchProject(_id);
}
componentWillReceiveProps({ project }) {
if (project) {
const { title, technology, description, creator } = project;
this.setState({ title, technology, description, creator });
}
}
onHandleSubmit = (event) => {
event.preventDefault();
event.stopPropagation();
const { history} = this.props;
this.props.editProject(this.props.match.params._id, this.state, history);
//this.props.history.push("/project");
}
render() {
return(
<div>
<form onSubmit={this.onHandleSubmit}>
<div className="input-field">
<input
value={this.state.title}
onChange={e => this.setState({ title: e.target.value })}
placeholder="Title"
/>
</div>
<div className="input-field">
<input
value={this.state.technology}
onChange={e => this.setState({ technology: e.target.value })}
placeholder="Technology"
/>
</div>
<div className="input-field">
<input
value={this.state.description}
onChange={e => this.setState({ description: e.target.value })}
placeholder="Description"
/>
</div>
<div className="input-field">
<input
value={this.state.creator}
onChange={e => this.setState({ creator: e.target.value })}
placeholder="Creator"
/>
</div>
<Link to="/project" className="red btn-flat white-text">
Cancel
</Link>
<button type="submit" className="teal btn-flat right white-text">
Submit
<i className="material-icons right">done</i>
</button>
</form>
</div>
);
}
}
// ownProps is the prop obj that is going to ProjectDetail component up top.
const mapStateToProps = ({ projects, auth }, ownProps) => {
return { auth, project: projects[ownProps.match.params._id]};
}
export default connect(mapStateToProps, actions)(withRouter(ProjectEdit));
ProjectDetail.js – using this component to display and delete a project
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchProject, deleteProject } from '../../actions';
import { Link } from 'react-router-dom';
import { withRouter } from "react-router-dom";
class ProjectDetail extends Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {
const { _id } = this.props.match.params;
this.props.fetchProject(_id);
}
onDeleteClick() {
console.log("Delete Project was clicked");
const { history } = this.props;
this.props.deleteProject(this.props.project._id, history);
}
render() {
const { project } = this.props;
if (!project) {
return <div>Loading...</div>;
}
const { title, technology, description, creator, datePosted } = project;
return (
<div>
<div className="row">
<div className="col s12 m9">
<h3>{title}</h3>
<h5>Technologies used: {technology}</h5>
<div className="card story">
<div className="card-content">
<span className="card-title">{new Date(datePosted).toLocaleDateString()}</span>
{description}
</div>
</div>
</div>
<div className="col s12 m3">
<div className="card center-align">
<div className="card-content">
<span className="card-title">{this.props.auth.displayName}</span>
<img className="circle responsive-img" src={this.props.auth.image}/>
</div>
</div>
</div>
</div>
<div className="row col s12">
<div className="col s6 offset-s1">
<Link className="waves-effect waves-light btn-large" to="/project">Back to project</Link>
</div>
<button className="btn-large red" onClick={() => this.onDeleteClick()}>Delete</button>
</div>
</div>
);
}
}
// ownProps is the prop obj that is going to ProjectDetail component up top.
function mapStateToProps({ projects, auth }, ownProps){
return { auth, project: projects[ownProps.match.params._id] };
}
export default connect(mapStateToProps, { fetchProject, deleteProject })(withRouter(ProjectDetail));
Project.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import ProjectList from './project/ProjectList';
import { Link } from 'react-router-dom';
class Project extends Component {
render() {
return(
<div>
<h2>Project</h2>
<ProjectList />
{this.props.auth ? <div className="fixed-action-btn">
<Link to="/project/new" className="btn-floating btn-large red">
<i className="material-icons">add</i>
</Link>
</div> : ''}
</div>
);
}
}
function mapStateToProps({ auth }) {
return { auth };
}
export default connect(mapStateToProps)(Project);
ProjectList.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchProjects } from '../../actions';
import { Link } from 'react-router-dom';
import _ from 'lodash';
class ProjectList extends Component {
componentDidMount() {
this.props.fetchProjects();
}
renderProjects() {
// return this.props.projects.reverse().map(project => {
return _.map(this.props.projects, project => {
return (
<div className="row" key={project._id}>
<div className="col s12 m6">
<div className="card">
<div className="card-image">
{this.props.auth ? <Link to={`/project/${project._id}/edit`} className="btn-floating halfway-fab waves-effect waves-light red">
<i className="material-icons">edit</i></Link> : ''}
</div>
<div className="card-content">
<span className="card-title">{project.title}</span>
<p>
<b>Technologies used: {project.technology} </b>
</p>
<p>
<b>Creator: {project.creator}</b>
</p>
<p>
<b>Project description: </b>{project.description}
</p>
<p className="right">
<b>Posted on: </b>{new Date(project.datePosted).toLocaleDateString()}
</p>
</div>
<div className="card-action">
<Link to={`/project/${project._id}`}>Read more</Link>
</div>
</div>
</div>
</div>
);
})
}
render() {
return (
<div>
{this.renderProjects()}
</div>
);
}
}
function mapStateToProps({ projects, auth }) {
return { projects, auth };
}
export default connect(mapStateToProps, { fetchProjects })(ProjectList);
Actions
export const editProject = (id, values, history) => async dispatch => {
const res = await axios.put(`/api/projects/${id}/edit`, values);
dispatch({ type: FETCH_PROJECTS, payload: res.data });
history.push('/project');
}
export const deleteProject = (id, history) => async dispatch => {
const res = await axios.delete(`/api/projects/${id}`);
dispatch({ type: FETCH_PROJECTS, payload: res.data });
history.push('/project');
}
Reducers
import mapKeys from 'lodash/mapKeys';
import { FETCH_PROJECT, FETCH_PROJECTS } from '../actions/types';
export default function(state = [], action) {
switch (action.type) {
case FETCH_PROJECTS:
return { ...state, ...mapKeys(action.payload, '_id') };
case FETCH_PROJECT:
const project = action.payload;
return { ...state, [project._id]: project };
default:
return state;
}
}
API
const mongoose = require('mongoose');
const requireLogin = require('../middlewares/requireLogin');
const Project = mongoose.model('projects');
module.exports = app => {
// show single project
app.get('/api/projects/:id', async (req, res) => {
const project = await Project.findOne({
_id: req.params.id
});
res.send(project);
});
// edit single project
app.put('/api/projects/:id/edit', requireLogin, async (req, res) => {
try {
const project = await Project.findOne({
_id: req.params.id
});
project.title = req.body.title;
project.technology = req.body.technology;
project.description = req.body.description;
project.creator = req.body.creator;
project.datePosted = Date.now();
project._user = req.user.id;
await project.save();
res.send(project);
} catch (err) {
res.status(422).send(err);
}
});
// fetch projects
app.get('/api/projects', async (req, res) => {
const projects = await Project.find({ _user: req.user.id });
// const projects = await Project.find({
// creator: "Theerut Foongkiatcharoen"
// });
res.send(projects);
});
// create a new project
app.post('/api/projects', requireLogin, async (req, res) => {
const { title, technology, description, creator } = req.body;
const project = new Project({
title,
technology,
description,
creator,
datePosted: Date.now(),
_user: req.user.id
});
try {
await project.save();
const user = await req.user.save();
res.send(user);
} catch (err) {
res.status(422).send(err);
}
});
// delete a single project
app.delete('/api/projects/:id', requireLogin, async (req, res) => {
try {
const project = await Project.remove({
_id: req.params.id
});
res.sendStatus(200);
} catch (err) {
res.status(422).send(err);
}
});
};
In the code:
const res = await axios.delete(`/api/projects/${id}`);
dispatch({ type: FETCH_PROJECTS, payload: res.data }.
Isn't this API endpoint returning only a HTTP status code, which you are then sending to reducer?
This explains your undefined key. Its undefined because you set it to non existing data from response.

Resources