I have a list of items in table. I want to be able to click on a details link on any item and go to a new page where I see the details of the item. Below is my code.
import React from 'react';
import { connect } from 'react-redux';
import toastr from 'toastr';
import { Link } from 'react-router';
import { bindActionCreators } from 'redux';
import * as documentActions from '../../actions/documentActions';
class DocumentDetailsPage extends React.Component {
constructor(props) {
super(props);
this.deleteDoc = this.deleteDoc.bind(this);
}
deleteDoc(id) {
this.props.actions.deleteDocument(id)
.then(res => toastr.success('Document deleted successfully!'));
}
render() {
const { document } = this.props;
return (
<div className="col s12">
<div className="card qBox">
<div className="card-content white-text">
<span className="card-title">{document.title}</span>
<p
dangerouslySetInnerHTML={{ __html: document.content }}
className="document-content"></p>
<br />
<p>Access Type:
<span>{(document.access).toUpperCase()}</span></p><br />
<div>
Published Date :
<p>{(document.createdAt) ?
document.createdAt.split('T')[0] : ''}</p>
<p id="owner">Author:
{document.owner.firstName} {document.owner.lastName}</p>
</div>
</div>
<div className="card-action">
<Link to="/">back</Link>
{this.props.auth.user.userId === document.ownerId &&
<div className="right">
<Link to={`/document/${document.id}`}>Edit</Link>
<Link to="/" onClick={this.deleteDoc()}> Delete </Link>
</div>
}
</div>
</div>
</div>
);
}
}
DocumentDetailsPage.propTypes = {
document: React.PropTypes.object.isRequired,
actions: React.PropTypes.object.isRequired,
auth: React.PropTypes.object.isRequired,
};
function getDocumentById(documents, id) {
const document = documents.filter(item => item.id === id);
if (document) return document[0];
return null;
}
function mapStateToProps(state, ownProps) {
const documentId = ownProps.params.id; // from the path `/document/:id`
let document;
if (documentId && state.documents.length > 0) {
document = getDocumentById(state.documents, parseInt(documentId, 10));
}
return {
document,
auth: state.auth,
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(documentActions, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(DocumentDetailsPage);
When I click on the details link, it should take me to this page with route document/:id where I can read the id from the route and fetch the corresponding document from state and render in the component. But when i go to this page, it tells me that document is undefined. Any reason for this? or is there a better way to get the param id from the route than using mapStateToProps?
You have two problems here:
1) Your deleteDoc class method expects an id argument which is passed to your action creator. Because you already have the full document as a prop, get it from the object instead of receiving it:
deleteDoc() {
const { actions, document } = this.props
actions.deleteDocument(document.id)
.then(res => toastr.success('Document deleted successfully!'));
}
2) You're CALLING the deleteDoc function in your onClick handler!
// Wrong
<Link to="/" onClick={this.deleteDoc()}> Delete </Link>
Remove the parens and simply set a reference to your function as the onClick prop, not the result from an actual call. :)
// Right
<Link to="/" onClick={this.deleteDoc}> Delete </Link>
Related
I'm learning React by making a Spotify clone and for now, what I'm trying to do is to show Spotify sections such as "last played songs", "top artists" and "top songs" through a component called Body.js.
I get the data from a Spotify official API library created by jmperez in a useEffect hook in the App.js component. Once I get the data from the API, I store it in an object called initialState in a file called reducer.js.
This reducer.js file contains the initial state and the reducer function for a custom hook called useDataLayer.js which is a useContext hook that passes as value a useReducer to all the branches of my program. This way what I do is update the initialState from App.js and access this object through the useDataLayer hook in the different branches of my program (among them the Body component).
The problem I am having now is that it is not rendering the three sections mentioned before in Spotify, but only shows me one which is the "top songs". The weird thing is that for a second it does render the other components as if it was getting the data and rendering but then it updates and they disappear. Please if someone can help me with this problem and explain to me why this happens it would be great.
App.js code
import React, {useState, useEffect} from 'react';
import Login from './components/Login';
import Player from './components/Player';
import { getTokenFromResponse } from './spotify';
import './styles/App.scss';
import SpotifyWebApi from "spotify-web-api-js";
import { library } from '#fortawesome/fontawesome-svg-core';
import { fas } from '#fortawesome/free-solid-svg-icons';
import { fab } from '#fortawesome/free-brands-svg-icons';
import { useDataLayer } from './components/Hooks/useDataLayer';
library.add(fas, fab);
//spotify library instance
const spotify = new SpotifyWebApi();
function App() {
const [token, setToken] = useState(null);
const [{ user }, dispatch] = useDataLayer();
// where I get the necessary data from the api
useEffect(() => {
// function to get access token
let accessToken = getTokenFromResponse();
window.location.hash = '';
if(accessToken){
spotify.setAccessToken(accessToken);
setToken(accessToken);
//FROM HERE I GET THE DATA I NEED
// here I get the data of my user
spotify.getMe().then((data) =>{
dispatch({
type: "GET_USER",
user: date
})
});
spotify.getUserPlaylists().then((data) => {
dispatch({
type: "GET_PLAYLISTS",
playlists: dates
})
});
spotify.getMyTopTracks({limit: 4}).then((data) => {
dispatch({
type: "GET_TOP_TRACKS",
top_tracks:data,
})
});
spotify.getMyRecentlyPlayedTracks({limit: 4}).then((data) => {
dispatch({
type: "RECENTLY_PLAYED",
recently_played: date,
})
});
spotify.getMyTopArtists({limit: 4}).then((data) => {
dispatch({
type: "GET_TOP_ARTISTS",
top_artists: date,
})
});
}
}, [token])
//if the token is valid enter Player.js where Body.js is inside and if not return to the login component
return (
<div className="App">
{ token ? <Player spotify= {spotify} /> : <Login />}
</div>
);
}
export defaultApp;
Body.js code
import React from 'react'
import '../styles/Body.scss'
import { useDataLayer } from "./Hooks/useDataLayer.js";
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome';
function Body({ spotify }) {
//get the properties of the necessary data that I want to display in this component with useDataLayer
const [{ spotify_recommendations, recently_played, top_tracks, top_artists }, dispatch] = useDataLayer();
return (
<div className= "main-body">
<div className= "body-option">
<span className= "see-more">See All</span>
<FontAwesomeIcon icon={['fas', 'arrow-right']} />
<div>
{
//to show the image and info of the track
recently_played?.items.map((item, index) =>{
return (
<div className= "track" key= {index}>
<img src= {item.track.album.images[1].url} alt= "recently played track"></img>
<div className= "track-data">
<h3>{item.track.name}</h3>
<p>{item.track.artists.map(artist => artist.name).join(", ")}</p>
</div>
</div>
)
})
}
</div>
</div>
<div className= "body-option">
<span className= "see-more">See All</span>
<FontAwesomeIcon icon={['fas', 'arrow-right']} />
<div>
{
//to show the image and info of the track
top_tracks?.items.map((topArtist, index) => {
return (
<div className= "track" key= {index}>
<img src= {topArtist.album.images[1].url} alt= "recently played track"></img>
<div className= "track-data">
<h3>{topArtist.name}</h3>
<p>{topArtist.artists.map(artist => artist.name).join(", ")}</p>
</div>
</div>
)
})
}
</div>
</div>
<div className= "body-option">
<span className= "see-more">See All</span>
<FontAwesomeIcon icon={['fas', 'arrow-right']} />
<div>
{
//to show the image and info of the artist
top_artists?.items.map((topTrack, index) => {
return (
<div className= "track" key= {index}>
<img src= {topTrack.images[1].url} alt= "recently played track"></img>
<div className= "track-data">
<h3>{topTrack.name}</h3>
<p>{topTrack.genres.join(", ")}</p>
</div>
</div>
)
})
}
</div>
</div>
</div>
)
}
export default Body
Code of my custom hook useDataLayer.js
import React, {useContext, createContext, useReducer} from 'react'
let DataContext = createContext();
export function DataLayer({reducer, initialState, children}) {
return (
<DataContext.Provider value= {useReducer(reducer, initialState)}>
{children}
</DataContext.Provider>
)
}
export let useDataLayer = () => useContext(DataContext);
SideBar.js: the component where i display the user's playlist
import React from 'react';
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome';
import { useDataLayer } from './Hooks/useDataLayer.js';
import '../styles/SideBar.scss';
function SideBar({ spotify }){
const [{ playlists }, dispatch] = useDataLayer();
return (
<div className= "side-bar">
<div className= "side-bar-options">
<div className= "spotify-logo">
<FontAwesomeIcon icon={['fab', 'spotify']} size= "3x"/>
<h1 className= "spotify-title">Spotify</h1>
</div>
<a className= "option" href= "./Player.js">
<FontAwesomeIcon icon={['fas', 'home']} />
<p className= "option-title" >Inicio</p>
</a>
<a className= "option" href= "./Player.js">
<FontAwesomeIcon icon={['fas', 'search']} />
<p className= "option-title">Buscar</p>
</a>
<a className= "option" href= "./Player.js">
<FontAwesomeIcon icon={['fas', 'headphones']} />
<p className= "option-title" >Tu Biblioteca</p>
</a>
</div>
<p className= "playlist-title">PLAYLISTS</p>
<div className= "playlists">
{
playlists?.items?.map(
(list, index) =>
<p className="playlists-option"
key={index}
>
{list.name}
</p>
)
}
</div>
</div>
)
}
export default SideBar;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
Player.js
import React from 'react';
import "../styles/Player.scss";
import SideBar from "./SideBar.js";
import Body from "./Body.js";
function Player({spotify}) {
return (
<div className= "player-container">
<div className= "player_body">
<SideBar spotify= {spotify} />
<Body spotify= {spotify} />
</div>
</div>
)
}
export default Player;
spotify.js: the code where i get the token from the URL
const authEndpoint = "https://accounts.spotify.com/authorize";
const clientId = '84c134b175474ddabeef0e0b3f9cb389'
const redirectUri = 'http://localhost:3000/'
const scopes = [
"user-read-currently-playing",
"user-read-recently-played",
"user-read-playback-state",
"user-top-read",
"user-modify-playback-state",
"user-follow-modify",
"user-follow-read",
"playlist-modify-public",
"playlist-modify-private",
"playlist-read-private",
"playlist-read-collaborative"
];
//obtain acces token from url
export const getTokenFromResponse = () => {
let params = new URLSearchParams(window.location.hash.substring(1));
let token = params.get("access_token");
return token;
};
//acces url
const accessUrl = `${authEndpoint}?client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scopes.join("%20")}&response_type=token&show_dialog=true`;
export default accessUrl;
Thank you very much for your time and attention.
By the look of it, your useEffect hook is being called twice in quick succession. It has the dependency of token, which starts as null for your first render. Inside the hook you then read the accessToken and set the state for it to your token. Doing this will trigger another render because the dependency changed from null to the value of your access token.
I would suggest to simply remove token from your useEffect dependency array (so that your hook acts as an "on mounted" event) and see if that gets you the desired effect. You should eventually move the getTokenFromResponse outside the hook, assuming it is not available on first render, as I'm just guessing it is immediately available.
Post comments, it might be better to separate your initializing code into a useEffect that only runs once, when the component is first mounted. This way, as I now suspect getTokenFromResponse() returns a new Object every call, you only need to call it once. Note, if it returns a Promise, this won't work, so you should verify this first before trying.
To better illustrate my point, I've put it in code with some comments:
useEffect(() => {
setToken(getTokenFromResponse());
// this only needs to happen once
window.location.hash = '';
}, []; // <- empty dependency array will only run once, on mount
useEffect(() => {
// out of the 2 times this will be called,
// it should only pass this condition on the last/2nd
if(token){
spotify.setAccessToken(token);
//FROM HERE I GET THE DATA I NEED
// here I get the data of my user
spotify.getMe().then((data) =>{
dispatch({
type: "GET_USER",
user: date
})
});
spotify.getUserPlaylists().then((data) => {
dispatch({
type: "GET_PLAYLISTS",
playlists: dates
})
});
spotify.getMyTopTracks({limit: 4}).then((data) => {
dispatch({
type: "GET_TOP_TRACKS",
top_tracks:data,
})
});
spotify.getMyRecentlyPlayedTracks({limit: 4}).then((data) => {
dispatch({
type: "RECENTLY_PLAYED",
recently_played: date,
})
});
spotify.getMyTopArtists({limit: 4}).then((data) => {
dispatch({
type: "GET_TOP_ARTISTS",
top_artists: date,
})
});
}
// this will trigger the effect any time token changes,
// which should now only be twice:
// 1st at it's default state value (null)
// 2nd after the previous useEffect sets its state
}, [token]);
Hope that helps but, if it's a promise, you'll need to handle it according (suggestion: moving the setToken into the then)
Good luck!
I am coding an app in which there is a collection of reviews and a person can respond to a review, but each review can only have one response. So far, I am doing this by rendering a ReviewResponseBox component in my ReviewCardDetails component and passing the review_id as props.
I have implemented the logic so that once there is one ReviewResponse, the form to write another will no longer appear. However, before I was initializing the state in this component with an empty array, so when I refreshed my page the response went away and the form came back up. (This is now commented out)
I am trying to resolve this by persisting my state using React LocalStorage but am having trouble writing my method to do this. Here is what I have so far:
Component that renders ReviewResponseBox and passes review_id as props:
import React from "react";
import './Review.css';
import { useLocation } from "react-router-dom";
import StarRatings from "react-star-ratings";
import ReviewResponseBox from "../ReviewResponse/ReviewResponseBox";
const ReviewCardDetails = () => {
const location = useLocation();
const { review } = location?.state; // ? - optional chaining
console.log("history location details: ", location);
return (
<div key={review.id} className="card-deck">
<div className="card">
<div>
<div className='card-container'>
<h4 className="card-title">{review.place}</h4>
<StarRatings
rating={review.rating}
starRatedColor="gold"
starDimension="20px"
/>
<div className="card-body">{review.content}</div>
<div className="card-footer">
{review.author} - {review.published_at}
</div>
</div>
</div>
</div>
<br></br>
<ReviewResponseBox review_id={review.id}/>
</div>
);
};
export default ReviewCardDetails;
component that I want to keep track of the state so that it can render the form or response:
import React from 'react';
import ReviewResponse from './ReviewResponse';
import ReviewResponseForm from './ReviewResponseForm';
import { reactLocalStorage } from "reactjs-localstorage";
class ReviewResponseBox extends React.Component {
// constructor() {
// super()
// this.state = {
// reviewResponses: []
// };
// }
fetchResponses = () => {
let reviewResponses = [];
localStorage.setResponses
reviewResponses.push(reviewResponse);
}
render () {
const reviewResponses = this.getResponses();
const reviewResponseNodes = <div className="reviewResponse-list">{reviewResponses}</div>;
return(
<div className="reviewResponse-box">
{reviewResponses.length
? (
<>
{reviewResponseNodes}
</>
)
: (
<ReviewResponseForm addResponse={this.addResponse.bind(this)}/>
)}
</div>
);
}
addResponse(review_id, author, body) {
const reviewResponse = {
review_id,
author,
body
};
this.setState({ reviewResponses: this.state.reviewResponses.concat([reviewResponse]) }); // *new array references help React stay fast, so concat works better than push here.
}
getResponses() {
return this.state.reviewResponses.map((reviewResponse) => {
return (
<ReviewResponse
author={reviewResponse.author}
body={reviewResponse.body}
review_id={this.state.review_id} />
);
});
}
}
export default ReviewResponseBox;
Any guidance would be appreciated
You would persist the responses to localStorage when they are updated in state using the componentDidUpdate lifecycle method. Use the componentDidMount lifecycle method to read in the localStorage value and set the local component state, or since reading from localStorage is synchronous directly set the initial state.
I don't think you need a separate package to handle this either, you can use the localStorage API easily.
import React from "react";
import ReviewResponse from "./ReviewResponse";
import ReviewResponseForm from "./ReviewResponseForm";
class ReviewResponseBox extends React.Component {
state = {
reviewResponses: JSON.parse(localStorage.getItem(`reviewResponses-${this.props.review_id}`)) || []
};
storageKey = () => `reviewResponses-${this.props.review_id}`;
componentDidUpdate(prevProps, prevState) {
if (prevState.reviewResponses !== this.state.reviewResponses) {
localStorage.setItem(
`reviewResponses-${this.props.review_id}`,
JSON.stringify(this.state.reviewResponses)
);
}
}
render() {
const reviewResponses = this.getResponses();
const reviewResponseNodes = (
<div className="reviewResponse-list">{reviewResponses}</div>
);
return (
<div className="reviewResponse-box">
{reviewResponses.length ? (
<>{reviewResponseNodes}</>
) : (
<ReviewResponseForm addResponse={this.addResponse.bind(this)} />
)}
</div>
);
}
addResponse(review_id, author, body) {
const reviewResponse = {
review_id,
author,
body
};
this.setState({
reviewResponses: this.state.reviewResponses.concat([reviewResponse])
}); // *new array references help React stay fast, so concat works better than push here.
}
getResponses() {
return this.state.reviewResponses.map((reviewResponse) => {
return (
<ReviewResponse
author={reviewResponse.author}
body={reviewResponse.body}
review_id={this.state.review_id}
/>
);
});
}
}
I have a simple user list with several details from the following api: https://gorest.co.in/public-api/users, where I want to add a selected user to a list of favorites. I am working with react-router to navigate between pages. Is this possible with React or do I also need Redux?
I have a complete LIVE EXAMPLE here with the user page and favorites.
Here is the code below for the user list:
import React from "react";
import axios from "axios";
import NavLinks from "./components/navLink";
export default class UserList extends React.Component {
constructor(props) {
super(props);
this.state = {
list: [],
addToFav: false
};
this.list = [];
}
componentDidMount() {
this.getList();
}
/* get users list */
getList = async () => {
const api =
"https://gorest.co.in/public-api/users?_format=json&access-token=3qIi1MDfD-GXqOSwEHHLH73Y3UitdaFKyVm_";
await axios
.get(api)
.then(response => {
this.list = response.data.result;
this.setState({
list: this.list
});
})
.catch(err => {
console.log(err);
});
};
addToFav = () => {
this.setState(
{
addToFav: !this.state.addToFav
},
() => console.log(this.state.addToFav)
);
};
render() {
let style = {
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(250px, 1fr))",
padding: "1rem",
gridGap: "1rem 1rem"
};
return (
<div>
<NavLinks />
<ul style={style}>
{this.state.list.map(user => {
return (
<li key={user.id}>
<div>
<img className="thumb" alt="" src={user._links.avatar.href} />
</div>
<div className="userInfo">
<p>
{user.first_name} {user.last_name}
</p>
</div>
<button onClick={this.addToFav}>Add to Favorites</button>
</li>
);
})}
</ul>
</div>
);
}
}
Thank you!
Here's a working codesandbox: https://codesandbox.io/s/brave-fire-4kd4p
This train of thought pretty much follows what #Chris G mentioned. Have a top-level state that holds the list of users and the favorites list. Then pass those as props to the individual components.
App.js
Hit your API here instead of inside your UserList component to prevent any unnecessary re-renders.
import React, { Component } from "react";
import UserList from "./userList";
import FavoriteList from "./favoriteList";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import axios from "axios";
export default class App extends Component {
state = {
list: [],
favorites: []
};
addFavorite = favorite => {
const { favorites } = this.state;
if (!favorites.some(alreadyFavorite => alreadyFavorite.id == favorite.id)) {
this.setState({
favorites: [...this.state.favorites, favorite]
});
}
};
getList = async () => {
const api =
"https://gorest.co.in/public-api/users?_format=json&access-token=3qIi1MDfD-GXqOSwEHHLH73Y3UitdaFKyVm_";
await axios
.get(api)
.then(response => {
this.setState({
list: response.data.result
});
})
.catch(err => {
console.log(err);
});
};
componentDidMount() {
this.getList();
}
render() {
return (
<Router>
<Switch>
<Route
path="/"
exact
render={() => (
<UserList list={this.state.list} addFavorite={this.addFavorite} />
)}
/>
<Route
path="/favorites"
render={() => <FavoriteList favorites={this.state.favorites} />}
/>
</Switch>
</Router>
);
}
}
UserList.js
Call the addFavorite event-handler on button-click to pass that item back up to the parent-state.
import React from "react";
import NavLinks from "./components/navLink";
export default class UserList extends React.Component {
render() {
let style = {
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(250px, 1fr))",
padding: "1rem",
gridGap: "1rem 1rem"
};
return (
<div>
<NavLinks />
<ul style={style}>
{this.props.list.map(user => {
return (
<li key={user.id}>
<div>
<img className="thumb" alt="" src={user._links.avatar.href} />
</div>
<div className="userInfo">
<p>
{user.first_name} {user.last_name}
</p>
</div>
<button onClick={() => this.props.addFavorite(user)}>
Add to Favorites
</button>
</li>
);
})}
</ul>
</div>
);
}
}
Favorite.js
Use the favorites array that was passed in as a prop and iterate over it.
import React from "react";
import NavLinks from "./components/navLink";
export default class FavoriteList extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
const { favorites } = this.props;
return (
<div>
<NavLinks />
<ul>
{favorites.map(user => {
return (
<li key={user.id}>
<div>
<img className="thumb" alt="" src={user._links.avatar.href} />
</div>
<div className="userInfo">
<p>
{user.first_name} {user.last_name}
</p>
</div>
</li>
);
})}
</ul>
</div>
);
}
}
Is this possible with React or do I also need Redux?
Most if not all of those problems can be solved without redux just by using component state. It just gets increasingly difficult to pass the state to the components needing it the more global state you have and the more components at different depth need to access and update it.
In your case it might be sufficient to store the favorites in a component state high up the tree and pass it to the components consuming it. You could either pass it directly to the components or you could use react context to make it accessible to components deep in the tree.
A simple example:
const FavoritesContext = React.createContext({favorites: []});
const FavoritesProvider = ({children}) => {
const [favorites, setFavorites] = useState([]);
const add = useCallback(favorite => setFavorites(current => [...current, favorite]), [setFavorites]);
return (
<FavoritesContext.Provider value={{favorites, add}}>
{children}
</FavoritesContext.Provider>
};
You can use it like that:
<FavoritesProvider>
<MyApp />
</FavoritesProvider>
then anywhere in a component in your app:
const MyComponent = () => {
const {favorites, add} = useContext(FavoritesContext);
const [draft, setDraft] = useState('');
const handleChange = event => setDraft(event.target.value);
const handleAdd = () => {
add(draft);
setDraft('');
};
return (
<div>
<ul>
{favorites.map(favorite => <li>{favorite}</li>)}
</ul>
<input value={draft} type="text" onChange={handleChange} />
<button onClick={handleAdd}>Add</button>
</div>
);
}
In this simple example the favorites are just text but they could as well be objects. Also it demonstrates how you could provide a handler for adding a favorite. You could implement e.g. a handler for removing favorites in the same way.
Persisting your favorites is yet another topic you may need to deal with. You could use e.g. localStorage for that or you could store that in a database on a server and fetch it when your app mounts the first time.
I have changed your file a bit take a look - https://codesandbox.io/s/clever-butterfly-vb2iz
One way is to use the localstorage of browser.
But this way is slighty expensive and synchronous.
Update the list whenever the favorited item status is changed via
localStorage.setItem('users',JSON.stringify(users));
And look for the favorited items via
localStorage.getItem('users');//You need to parse this by JSON.parse()
Maintain a isFavorite variable in the object list.
let users=[{name:"Mr.A",isFavorite:false},{name:"Mr.B",isFavorite:true},...];
On the click of favoriting button this.addToFav change it as follows
addToFav=user=>{
const {users}=this.state;
this.setState({
users:users.map(userObject=>userObject.id===user.id?
{...userObject,isFavorite:!userObject.isFavorite}:user)
},()=>{saveToLocal(this.state.users)});
}
Now you can access the favorite items even if the page is reloaded and stays there till you clear the storage.Use this localStorage.clear() for that.
First I would change your onClick to this:
<button onClick={() => this.addToFav(user.id)}>Add to Favorites</button>
This will allow you to pass the id to the addToFave function.
Then I would add a new state called faves (an array) and every time someone clicks the add button I would add their id into this array. This will allow you to filter your original list when you want to display the faves.
this.state = {
list: [],
faves: [],
};
}
addToFav = (id) => {
this.setState(prevState => ({
faves: [...prevState.faves, id],
}));
};
When I want to use the list of faves instead of the normal list I would do this:
const favesList = [];
this.state.list.map(listItem =>
this.state.faves.find(
faveId => listItem.id === faveId
) && favesList.push(item);
Then I would pass that to the faves component
I changed accordingly, please try
https://codesandbox.io/s/youthful-poincare-7oeh0
the key is you can use push state to your link like below
<Link to={{ pathname: "/favorites", state: { favList: this.props.favList }}} onClick={() => this.forceUpdate()}>
later on under your fav page call to retrieve the state
this.props.location.state.favList
i have changed the code a little by using react context.
I would not use redux for this cause i think it would be a overkill.
Anyways here is the updated sandbox...
Link for sandbox
I'm practicing react and redux and I'm creating a simple app where I have a sidebar showing a list of categories that is visible on every route and the main area that initially displays all the books I have and when clicking on a category link on the sidebar the main area loading another component with all the books related to this category.
Here's my routes setup in the App.js file ...
class App extends Component {
async componentDidMount() {
try {
await this.props.asyncLoadBooks();
await this.props.asyncLoadCategories();
} catch (error) {
console.log(error);
}
}
render() {
return (
<>
<Header />
<div className="global-wrapper">
<div className="container">
<aside className="side-bar">
<Categories />
</aside>
<main className="main-content">
<Switch>
<Route exact path="/" component={Books} />
<Route
exact
path="/category/:id"
component={Category}
/>
<Route component={NotFound} />
</Switch>
</main>
</div>
</div>
</>
);
}
}
In the App.js as you can see I'm loading the data via a local JSON file with axios in the Actions files of the booksActions and categoriesAction, it's pretty straightforward.
And here's the Categories component ...
class Categories extends Component {
render() {
const { categories } = this.props;
let categoriesList;
if (categories && categories.length !== 0) {
categoriesList = categories.map(category => (
<li key={category.id}>
<Link to={`/category/${category.id}`}>{category.name}</Link>
</li>
));
} else {
categoriesList = <Loading />;
}
return (
<div>
<h2>Categories</h2>
<ul>{categoriesList}</ul>
</div>
);
}
}
const mapState = state => ({
categories: state.categories.categories
});
export default connect(mapState)(Categories);
And I'm firing another action in the ComponentDidMount() of the single Category component to get all the books related to that component and render them ...
class Category extends Component {
componentDidMount() {
this.props.getCategoryBooks(this.props.match.params.id);
}
componentDidUpdate(prevProps) {
if (prevProps.match.params.id !== this.props.match.params.id) {
this.props.getCategoryBooks(this.props.match.params.id);
}
}
render() {
const { categoryBooks } = this.props;
return (
<div>
{/* <h1>{this.props.match.params.id}</h1> */}
{categoryBooks &&
categoryBooks.map(book => {
return <div key={book.id}>{book.title}</div>;
})}
</div>
);
}
}
const mapState = state => ({
categories: state.categories.categories,
categoryBooks: state.books.categoryBooks
});
const mapActions = {
getCategoryBooks
};
export default connect(
mapState,
mapActions
)(Category);
Now, everything is working the first time, however, when I click on another category the <Category /> component doesn't get updated because I'm dispatching the action in the componentDidMount() thus the component already mounted the first time, so it doesn't dispatch the action again after I click on another category, now what is the best way to handle this?
The second issue is where I'm on a category route http://localhost:3000/category/9967c77a-1da5-4d69-b6a9-014ca20abd61 and I try to refresh the page, the categoris list loads fine on the sidebar, but the single component shows empty, and when I look on the redux-devtools I find that the GET_CATEGORY_BOOKS action gets fired before the LOAD_BOOKS and LOAD_CATEGORIES in the App.js file, because the child componentDidMount() method gets called before its parent equivalent method. How to solve this as well?
I hope you guys can help me in this.
Edit
As ##NguyễnThanhTú noticed, the componentDidupate had a typo, now it works when clicking on another category.
That leaves us with the second issue when reloading the page in the category route and the data doesn't show because of the App.js componentDidMount fires after its children components.
Edit
Here's a repo on Github for this project ...
https://github.com/Shaker-Hamdi/books-app
In your booksActions.js, add this:
export const getCategoryBooksV2 = categoryId => {
return async (dispatch, getState) => {
const { books } = getState();
if (books.books.length === 0) {
console.log('Only executing once') // testing purpose only
const response = await axios.get("books.json");
const data = response.data.books;
dispatch(loadBooks(data));
dispatch(getCategoryBooks(categoryId));
}
dispatch(getCategoryBooks(categoryId));
};
};
In your Category.js, use that new action creator:
import { getCategoryBooksV2 } from "../books/booksActions";
...
componentDidMount() {
this.props.getCategoryBooksV2(this.props.match.params.id);
}
...
const mapActions = {
getCategoryBooksV2
};
This solution is inspired by this example:
function incrementIfOdd() {
return (dispatch, getState) => {
const { counter } = getState();
if (counter % 2 === 0) {
return;
}
dispatch(increment());
};
}
From the Redux-Thunk Documentation
This is the demo:
Okay so this is driving me crazy ! Had to restart coding the project from scratch to pinpoint where the problem is.
Basically I'm trying to practice React by building a web app where I can share spotify songs. So here's my Component tree (only the important components: App.js -> [Navbar, Posts] -> then inside Posts i have a list of Post components. Here are the codes:
import React, { Component } from 'react';
import './App.css';
import {BrowserRouter} from 'react-router-dom';
import Navbar from './components/Navigation/Navbar';
import Posts from './containers/Posts/Posts';
class App extends Component {
render() {
return (
<BrowserRouter>
<div className="App">
<Navbar />
<Posts />
</div>
</BrowserRouter>
);
}
}
export default App;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
import React, { Component } from 'react';
import Post from './Post/Post';
import $ from 'jquery';
class Posts extends Component {
state = {
posts: null,
// addingNewPost: false
}
componentDidMount() {
$.ajax({
url: 'https://music-blog-app.firebaseio.com/users/user/posts.json',
success: (response) => {
//console.log(response); // object of objects
// converting to array of objects
const responseArray = Object.keys(response).map(i => response[i]);
//console.log(responseArray);
this.setState({
posts: responseArray
})
// console.log(this.state.posts);
}
//error
});
}
// addingNewPostHandler = () => {
// this.setState({addingNewPost: true});
// }
// cancelNewPostHandler = () => {
// this.setState({addingNewPost: false});
// }
sharedNewPostHandler = (caption, embedSrcLink) => {
var newPostToAdd = {
caption: caption,
embedSrcLink: embedSrcLink
}
var postsToUpdate = this.state.posts.slice();
postsToUpdate.push(newPostToAdd);
// $.ajax({
// type: 'POST',
// url: 'https://music-blog-app.firebaseio.com/users/user/posts.json',
// success: (response) => {
// console.log(response);
// this.setState(prevState =>({
// addingNewPost: false,
// posts: [...this.state.posts, newPostToAdd]
// }));
// }
// // error
// });
}
render() {
var postsToRender = <p>Nothing here</p>
console.log(this.state.posts);
if(this.state.posts) {
var myPosts = this.state.posts.slice();
}
console.log(myPosts);
let render;
if(myPosts) {
render = (myPosts.map((post, index) => { return <p>IF I REPLACE THIS BY RENDERING POST component, I get an infinite loop</p>}))
} else {
render = <p>still waiting...</p>
}
return (
<div className="container posts-container">
{/* <p>jsdhfjhd</p>
{myPosts ? (myPosts.map((post, index) => {
// console.log(post)
return <Post key={post} caption={this.state.posts[index].caption} embedSrcLink={this.state.posts[index].embedSrcLink} />
})) : <p>still waiting...</p>} */}
{render}
</div>
);
}
}
export default Posts;
import React, { Component } from 'react';
import './Post.css';
import PosterProfile from '../../../components/PosterProfile/PosterProfile';
const post = (props) => (
<div className="post">
<PosterProfile />
<div className="card" style={{width: '18rem'}}>
<div className="card-body">
<h5 className="card-caption">{props.caption}</h5>
<div className="embed-iframe">
<iframe title="embed" src={props.embedSrcLink} width="300" height="380" frameBorder="0" allowtransparency="true" allow="encrypted-media"></iframe>
</div>
</div>
<div className="card-footer">
Like
Comment
Repost
</div>
</div>
</div>
)
export default post;
Here is the problem !! So this piece of code inside the render method of Posts:
render = (myPosts.map((post, index) => { return <p>IF I REPLACE THIS BY RENDERING POST component, I get an infinite loop</p>}))
AS SOON AS I replace it with
render = (myPosts.map((post, index) => {
return <Post key={post} caption={this.state.posts[index].caption} embedSrcLink={this.state.posts[index].embedSrcLink}
}))
I am getting the posts from a firebase database by the way.
Please help ! Thank you in advance :)
Rendering a list/array in react requires you to add a key to each item. From the docs:
Keys help React identify which items have changed, are added, or are removed. Keys should be given to the elements inside the array to give the elements a stable identity
https://reactjs.org/docs/lists-and-keys.html#keys
I believe what is happening in your snippets, is that you're assigning a key as an object instead of a string. This would definitely cause unexpected behavior or errors.