I'm trying to implement a follow and unfollow button in my app.
I've been trying to update it in the state, but when I click the follow button it changes all the user's buttons (not only the one clicked).
Now I'm a bit confused on how to show unfollow button if the user is already following the other user and make it work when clicked.
Here is my code:
class Followers extends React.Component {
constructor(props) {
super(props)
this.state = {
users: [],
follower: [],
following: [],
button: "Follow"
}
}
componentDidMount = () => {
this.getUsers()
}
getUsers = () => {
axios(`http://localhost:7001/api/users`)
.then(response => {
this.setState({ users: response.data})
console.log(response.data)
})
.catch(error => {
this.setState({ error: true })
})
}
followUser = (e) => {
e.preventDefault();
const userId = this.props.user[0].id
const followedId = e.target.value
axios.post(`http://localhost:7001/api/users/${userId}/follow/${followedId}`, {
userId,
followedId,
createdAt: new Date().toISOString().slice(0, 10),
updatedAt: new Date().toISOString().slice(0, 10)
})
.then(response => {
console.log(response.data)
this.setState(state => ({
button: "Unfollow",
loggedIn: !state.loggedIn
}))
})
.catch(error => {
console.log(error)
})
}
unfollowUser = (e) => {
e.preventDefault();
const userId = this.props.user[0].id
const followedId = e.target.value
axios.delete(`http://localhost:7001/api/users/${userId}/unfollow/${followedId}`)
.then(response => {
console.log(response)
this.setState({ button: "Follow" })
})
.catch(error => {
this.setState({ error: true })
})
}
render() {
const { users, button } = this.state
const userId = this.props.user[0].id
return (
<div>
<h2>Users in Unax</h2>
<ul>
{users.map((user, index) => {
if(user.id !== userId) {
return (
<Card className="users" key= {index}>
<CardBody>
<CardTitle>{user.user_name}</CardTitle>
<Button id="btn" value={user.id} onClick={this.followUser}>Follow</Button>
<Button id="btn" value={user.id} onClick={this.unfollowUser}>Unfollow</Button>
</CardBody>
</Card>
)}
})}
</ul>
</div>
)
}
}
<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>
My last try was to add two buttons and make a conditional, but I cannot think a way to compare if the relationship already exists, should this be done in the backend?
It looks as though all of your follow/unfollow buttons are linked to the same single piece of state, meaning that when its true they are all true and when false they are all false.
One way to achieve this is to have a 'followed' property in the user object. Then you can alter the button based on whether that user is already being followed or not.
You can then update the database and the local state to give the user the most responsive experience.
For example your user object could look something like:
{id: 1, name: Bob, followed: false, image: ....}
This would allow you to understand what state your button should be in.
In depth description of managing a friendship database
Related
I added like and unlike buttons to my react app. I'm using redux to manage the state and storing the data in firebase realtime-database. The buttons are working as they should but I need to reload the page to show the post has been liked/unliked, it is not re-rendering on its own. I tried using both forceUpdate and setState but both didn't work.
postLiked = (id) => {
this.props.onLikePost(this.props.user, id)
this.forceUpdate()
}
postUnliked = (id, unlikeID) => {
this.props.onUnlikePost(id, unlikeID)
}
render() {
{this.props.data.map((res) => {
const liked = [];
for(let key in res.LikedBy){
liked.push({
...res.LikedBy[key],
id: key
});
}
let boolButton = false;
if(liked.filter((val) => {
if(val.username === this.props.user) {
boolButton = true
}
}))
return(
<div>
<div className="bottomButtons">
{boolButton ? <Button className="btn btn-primary likeDislike"
id="likeButton"
onClick={() => this.postUnliked(res.id, liked.find((val) => {
if(val.username === this.props.user){
return val.id;
}
}))}
>
<FontAwesomeIcon icon={faThumbsUp} style={{width:"13.5px", color:"white"}}/>
</Button> : <Button className="btn btn-light likeDislike"
id="likeButton"
onClick={() => this.postLiked(res.id)}
>
<FontAwesomeIcon icon={faThumbsUp} style={{width:"13.5px"}}/>
</Button>
}
These are the action functions
export const likePost = (username, postId) => {
const req = {
username,
postId
}
return (dispatch) => {
axios.post('/Data/' + postId + '/LikedBy.json', req)
.then((res) => {
dispatch({
type: actionTypes.Like_Post,
payload: res
})
})
}
}
export const unlikePost = (id, unlikeId) => {
return (dispatch) => {
axios.delete('/Data/' + id + '/LikedBy/' + unlikeId.id + '.json')
.then((res) => {
dispatch({
type: actionTypes.Unlike_Post
})
}).catch((error) => {
console.log(error)
})
}
}
And this is the reducer function
const initialState = {
Data: []
}
const reducer = (state = initialState, action) => {
switch(action.type){
case actionTypes.Like_Post:
return {
...state,
Data: state.Data.map((post) => post.id === action.payload.postId
? {...post, LikedBy: post.LikedBy.concat(action.payload.username)}:post),
loading: false,
}
case actionTypes.Unlike_Post:
return {
...state,
Data: state.Data,
loading: false,
}
EDIT
I tried other methods but nothing is working. The issue is with the reducer and I am not correctly updating the state. I tried updating the LikedBy field but I only get an error.
Tried this approach but I got an error saying res.map is not a function
case actionTypes.Like_Post:
return {
...state,
Data: state.Data.forEach((res) => res.map((q) => {
if(q.id === action.payload.postId) {
q.LikedBy.concat(action.payload.username)
}
return q
})
)
}
You shouldn't be re-loading the page to upload this state really (don't listen to people who tell you to do it); this is one of the many problems that React was designed to solve. The reason you are having an issue is because your component isn't SEEING a change to its data therefor a re-render is not being triggered, this is most likely because you aren't passing the correct prop into your component.
Your redux reducer is referring to Data but locally in your component you are using this.props.data try making sure you are actually passing your data reducer properly into the component.
Goal:
When you are pressing the button 'GO HOME!' you should execute the code "fetch(https://jsonplaceholder.typicode.com/users)" and then being pushed to '/home'
Problem:
How do I apply that the button can execute the fetch and push at the same time?
Info:
*I'm new in react JS
Stackblitz:
https://stackblitz.com/edit/react-router-history-push-yux12z?file=components/User.js
import React from "react";
export class User extends React.Component {
fetchUsers() {
fetch(`https://jsonplaceholder.typicode.com/users`)
.then(response => response.json())
.then(data =>
this.setState({
users: data,
isLoading: false
})
)
.catch(error => this.setState({ error, isLoading: false }));
}
render() {
return (
<div>
<h3>THE USER PAGE</h3>
<p>USER ID: {this.props.match.params.id}</p>
<button
onClick={() => {
this.props.history.push("/home");
}}
>
GO HOME!
</button>
</div>
);
}
}
The setSate function takes another optional input, a function to be executed after the state was updated.
import React from "react";
export class User extends React.Component {
fetchUsers() {
fetch(`https://jsonplaceholder.typicode.com/users`)
.then(response => response.json())
.then(data =>
this.setState({
users: data,
isLoading: false
}, () => {
// this will be executed after the state was updated
// the data was fetched and the state was update
this.props.history.push("/home")
})
)
.catch(error => this.setState({ error, isLoading: false }, ()=>{
// the state was updated, you can redirect to home or an error page
this.props.history.push("/home")
}));
}
render() {
return (
<div>
<h3>THE USER PAGE</h3>
<p>USER ID: {this.props.match.params.id}</p>
<button
onClick={this.fetchUsers.bind(this)}
>
GO HOME!
</button>
</div>
);
}
}
I'm trying to filter an array with fetched list of users. Users are stored in component state. I want to filter it by text from input.
Problem: When I enter letters the list is filtering, but when I delete letters result remains unchanged.
Thanks for help!!!
class App extends React.Component {
state = {
isLoading: true,
users: [],
error: null
}
fetchUsers() {
fetch(`https://jsonplaceholder.typicode.com/users`)
.then(response => response.json())
.then(data =>
this.setState({
users: data,
isLoading: false,
})
)
.catch(error => this.setState({ error, isLoading: false }));
}
componentDidMount() {
this.fetchUsers();
}
onChangeHandler(e) {
console.log(e.target.value);
let newArray = this.state.users.filter((d)=>{
let searchValue = d.name.toLowerCase();
return searchValue.indexOf(e.target.value) !== -1;
});
console.log(newArray)
this.setState({
users:newArray
})
}
render() {
const {isLoading, users, error} = this.state;
return (
<div>
<h1>Users List</h1>
<input type="text" value={this.state.value} placeholder="Search by user name..." onChange={this.onChangeHandler.bind(this)}/>
{error ? <p>{error.message}</p> : null}
<ol>
{!isLoading ? (
users.map(user => {
const {username, name, id} = user;
return (
<li key={id}>
<p>{name} <span>#{username}</span></p>
</li>
);
})
) : (
<h3>Loading...</h3>
)}
</ol>
</div>
);
}
}
export default App;
To achieve expected result, use below option of storing the users list from API to a variable after using setState to update users
apiUsers = [];
fetchUsers() {
fetch(`https://jsonplaceholder.typicode.com/users`)
.then(response => response.json())
.then(data =>{
this.apiUsers = data;
this.setState({
users: data,
isLoading: false,
})
}
)
.catch(error => this.setState({ error, isLoading: false }));
}
Instead of filtering this.state.users use the newly created variable - apiUsers in onChangeHandler
onChangeHandler(e) {
console.log(e.target.value);
let newArray = this.apiUsers.filter((d)=>{
console.log(d)
let searchValue = d.name.toLowerCase();
return searchValue.indexOf(e.target.value) !== -1;
});
console.log(newArray)
this.setState({
users:newArray
})
}
working code for reference - https://stackblitz.com/edit/react-sncf1e?file=index.js
Issue: : state.users array is getting updated without the copy of actual users list from api
It happens because you do not keep a track of the original values you get from the API. Once you start filtering it, you lose the full list.
I created a codesandbox: https://codesandbox.io/s/nostalgic-sunset-jig33 to fix your issue.
What I did is to add a initialUsers value in the state and use it to store the value coming from the API:
state = {
isLoading: true,
initialUsers: [],
users: [],
inputValue: "",
error: null
};
fetchUsers() {
fetch(`https://jsonplaceholder.typicode.com/users`)
.then(response => response.json())
.then(data =>
this.setState({
initialUsers: data,
isLoading: false
})
)
.catch(error => this.setState({ error, isLoading: false }));
}
and in the render method, I switch between displaying the filtered list of the original list by checking if a text is entered in the input element
(!!inputValue ? users : initialUsers).map(
user => {
const { username, name, id } = user;
return (
<li key={id}>
<p>
{name} <span>#{username}</span>
</p>
</li>
);
}
)
Well, if you logically look at your current solution then you have the following situation
users is an empty array, with isLoading true
users are fetched and pushed in the array, lets assume ['John', 'Joe', 'Alfred']
you filter your users for the letter J and update the user state to ['John', 'Joe']
You remove the filter for letter J and update the user state to ['John', 'Joe'] cause the component completely forgot about Alfred, you deleted hem in step 3
So, you have several options, just filter the render before the mapping, depending on how many users, that shouldn't be that bad or create more than 1 property on the state, say filteredUsers which contains the list of users that match your filter, and work with that state instead.
The cool thing about your current filter, is that it will always go faster, as your resultset kind of trims down, so that's a good thing, but I doubt you'll be dealing with so many users that you need this.
So far I am calling an api in componentDidMount() and set it to select option.
also call another conditional api from user input.
But Problem is it is calling the api non stop.
**getRates(){
const base = this.handlePrint(this.state.value);
fetch(`https://exchangeratesapi.io/api/latest?base=${base}`)
.then(data => data.json())
.then(data =>{
this.setState({
rate: data.rates,
})
console.log(data.rates)
})
.catch(err => console.log(err))
}**
And my console screen shot:
console
I just need one time api call based on user input.
full code: https://codeshare.io/5MwXzq
I think there is a problem with the state but I am new in reactjs and could not understand how to solve that.
Anyone can help please.
This is happening not because of anything in componentDidMount()
Based on the code you shared on codeshare.io, you're calling getRates() function in your render() method. Also, you're setting the state using setState within the getRates method. This causes a re-render, calling render() again, and so you get the infinite loop of calls.
Remove the call to getRates() from your render method and it'll work.
EDIT:
Since there were small changes but strewn across your code to get it to work, I've modified your class and posting it here:
class Main extends React.Component {
constructor(props) {
super(props);
this.state = {
currencies: [],
value: "?",
base: "?",
input: "?",
rate: 0
};
this.getRates = this.getRates.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handlePrint = this.handlePrint.bind(this);
}
componentDidMount() {
fetch("https://exchangeratesapi.io/api/latest?symbols=USD,GBP,AUD,JPY")
.then(data => data.json())
.then(data => {
const currencies = [];
currencies.push(
data.base,
...Object.entries(data.rates).map(rates => rates[0])
);
this.setState({
currencies
});
})
.catch(err => console.log(err));
}
getRates() {
const base = this.handlePrint();
console.log(this.state); fetch(`https://exchangeratesapi.io/api/latest?base=${base}`)
.then(data => data.json())
.then(data => {
this.setState({
rate: data.rates
});
console.log(data.rates);
})
.catch(err => console.log(err));
}
//Dropdown
DropDown = function(list) {
return <option value={list}>{list}</option>;
};
handleChange(e) {
this.setState({ value: e.target.value });
}
handlePrint() {
console.log(this.state)
if (this.state.value) {
return this.state.value;
}
};
render() {
const { currencies } = this.state;
// const input = this.getRates();
return (
<div>
<span>SELECT your Base: </span>
<select autoFocus onChange={this.handleChange}>
<option inputcurrency={currencies} selected data-default>
SELECT BASE
</option>
{currencies.map(this.DropDown)}
</select>
<button onClick={this.getRates}>GET RAtes</button>
<p>selected base:{this.handlePrint()} </p>
</div>
);
}
}
The changes are:
1. Bound getRates() method in the constructor
2. Removed the call to getRates() in render start
3. Removed unnecessary items passed to handlePrint
4. Changed the button onClick to point to getRates
I'm new to react. I have a component here that will automatically load the data from a database (mongo) via express server url when the page was loaded.
componentDidMount() {
var self = this;
fetch('/movies')
.then((resp) => resp.json())
.then(movies => self.setState({
movies: movies
}))
.catch((err) => console.log(err));
}
this will load all data and display them on screen and when the user types in to the textbox it will update the display to the search term:
movieSearch(term){
var self = this;
axios.get('/moviesbytitle', {
params: {
term: term
}
})
.then(movies => self.setState({
movies: movies.data
}))
.then(() => console.log('this is is a state >>>>+++', self.state))
.catch((err) => console.log(err));
}
render(){
const movieSearch = _.debounce((term) => { this.movieSearch(term) }, 300);
return (
<div>
<MovieTopBar />
<SearchBar onSearchTermChange={movieSearch}/>
<MovieList movies={ this.state.movies }/>
<MovieFooter />
</div>
);
}
};
class SearchBar extends Component{
constructor(props){
super(props);
this.state = { term: '' };
this.onInputChange = this.onInputChange.bind(this);
}
<input type="text" value={this.state.term} onChange={event => this.onInputChange(event.target.value)} className="form-control" id="" placeholder="Enter the title of the movie..." />
onInputChange(term){
this.setState({ term });
this.props.onSearchTermChange(term);
}
}
I am trying to find a way how can I update the data searched -> to the data when it was first mounted w/c displays the whole data whenever the textbox for search is emptied. Is there a specific lifecycle method to do that? or do I need to do a separate function? Thanks for the help in advance!
I would suggest
this.movieSearch = _.debounce((term) => { this.movieSearch(term) }, 300);
move it to constructor
so you can avoid creating a new function each render, this will be expensive on memory
you also do not need var self = this;
using anonymous functions like () => {} will preserve this
and coming back to question, if I understood it, you want to show original movie list when user deletes input in search,
componentDidMount() {
var self = this;
fetch('/movies')
.then((resp) => resp.json())
.then(movies => {
self.setState({ movies: movies });
this.originalResults = movies;
})).catch((err) => console.log(err));
}
you can do it by keeping reference to original results
and then you can add
movieSearch(term){
if (term === '') {
this.setState({movies:this.originalResults})
return; // exit the search function
} ...rest of function
As #AlexGvozden said, you should move your movieSearch to be part of the component class, so
class ShowMoviesList {
constructor() {
this.movieSearch = this.movieSearch.bind(this);
}
componentDidMount() {
this.getAllMovies();
}
getAllMovies() {
fetch('/movies')
.then((resp) => resp.json())
.then(movies => this.setState({
movies: movies
}))
.catch((err) => console.log(err));
}
getMovieByTitle(term) {
axios.get('/moviesbytitle', {
params: {
term: term
}
})
.then(movies => this.setState({
movies: movies.data
}))
.then(() => console.log('this is is a state >>>>+++', this.state))
.catch((err) => console.log(err));
}
movieSearch(term) {
// remember prevents blank spaces
const currentTerm = term ? term.trim() : term;
// remember clear last getMovieByTitle call
if (this.timer) {
clearTimeout(this.timer);
}
// if term is empty, so call getAllMovies again
// prevent call cache movies, because cache movie will always return the first call
// but won't render new movies if exists
if (!currentTerm) {
return this.getAllMovies();
}
this.timer = setTimeout(() => { this.getMovieByTitle(currentTerm); }, 300);
}
render() {
return (
<div>
<MovieTopBar />
<SearchBar onSearchTermChange={this.movieSearch}/>
<MovieList movies={ this.state.movies }/>
<MovieFooter />
</div>
)
}
}
if better call getAllMoviesfunction again when termis empty, because you will show the real current movies instead a cache movies version from first call.
I hope it can help you.