I'm trying to increment the number I get from this.props.match.params.id to get the DOM to re-render with new API fetched content. I was trying to do that by calling a function on Click and incremeting value by one. However, that's not happening at all, instead of incrementing the value, it concatenates like this:
https://imgur.com/a/dnKScN0
This is my code:
export class Sofa extends React.Component {
constructor(props) {
super(props);
this.state = {
token: {},
isLoaded: false,
model: {}
};
}
addOne = () => {
console.log('addOne');
this.props.match.params.id += 1;
console.log('id: ', this.props.match.params.id);
}
lessOne = () => {
console.log('lessOne');
}
componentDidMount() {
/* fetch to get API token */
fetch(url + '/couch-model/' + this.props.match.params.id + '/', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'JWT ' + (JSON.parse(localStorage.getItem('token')).token)
}
}).then(res => {
if (res.ok) {
return res.json();
} else {
throw Error(res.statusText);
}
}).then(json => {
this.setState({
model: json,
isLoaded: true
}, () => { });
})
}
render() {
const { model, isLoaded } = this.state;
if (!isLoaded) {
return (
<div id="LoadText">
Loading...
</div>
)
} else {
return (
<div id="Sofa">
/* other HTML not important for this matter */
<img src="../../../ic/icon-arrow-left.svg" alt="Arrow pointing to left" id="ArrowLeft"
onClick={this.lessOne}/>
<img src="../../../ic/icon-arrow-right.svg" alt="Arrow pointing to right" id="ArrowRight"
onClick={this.addOne}/>
</div>
);
}
}
}
You can't manipulate props, they are immutable. You have a few options:
Use Redux or some other form of global state management and store the counter there.
Store the counter in the state of the parent, pass it down as a prop to the child along with a callback function allowing the child to increment the counter.
Store the state for the counter directly within the child component and update it there.
As your string is concatenating JS thinks its a string and not a number. Just increment like so Number(this.props...id) + 1
It seems that this.props.match.params.id is string. Adding a value to string variable works as concatination.
You need to parseInt before adding a value like:
this.props.match.params.id = parseInt(this.props.match.params.id) + 1;
Very likely this.props.match.params.id is a string. Doing this: '1'+1 returns '11'.
I guess this is a 'main' component - just 'below' router. You can't modify param from router - you should generate link to next item (pointing the same component) which will get it's own props ('updated' id) from router.
You're sure you will have contiuous range of ids? No deletions in db? Rare ;)
Normally you have a searched/category based list of items. Browsing list ... entering item (by id), back to list, enter another item. Browsing items w/o info about list is a little harder and inc/dec id isn't good idea.
Get list into main component, pass current id to sub-component (able to load data itself and render it). Prepare next/prev buttons in main (modify current in state, loop them?) - changed prop (id) will force rendering component to load next data.
Further ... extract next/prev into sub-components, passing handlers from main (you didn't passed them and even if it won't work for reasons above).
Accessing items only by id is a bit harder, f.e. you can 'ask' api for next/prev items for current one. It can be done async - render next/prev when data will be ready.
Don't make all in one step ;)
Related
I have this page which shows a single post and I have a like button. if the post is liked, when the user clicks the button, it changes its state to unlike button, but if the post is not liked, then the like is getting registered and the id is getting pushed on to the array, but the button state is not getting updated and I have to reload the page to see the page. Can someone tell me how to resolve this issue?
This is the code:
const [liked, setLiked] = useState(false)
const [data, setData] = useState([]);
function likePosts(post, user) {
post.likes.push({ id: user });
setData(post);
axiosInstance.post('api/posts/' + post.slug + '/like/');
window.location.reload()
}
function unlikePosts(post, user) {
console.log('unliked the post');
data.likes = data.likes.filter(x => x.id !== user);
setData(data);
return (
axiosInstance.delete('api/posts/' + post.slug + '/like/')
)
}
For the button:
{data.likes && data.likes.find(x => x.id === user) ?
(<FavoriteRoundedIcon style={{ color: "red" }}
onClick={() => {
unlikePosts(data, user)
setLiked(() => liked === false)
}
}
/>)
: (<FavoriteBorderRoundedIcon
onClick={() => {
likePosts(data, user)
setLiked(() => liked === true)
}
}
/>)
}
Thanks and please do ask if more details are needed.
As #iz_ pointed out in the comments, your main problem is that you are directly mutating state rather than calling a setState function.
I'm renaming data to post for clarity since you have said that this is an object representing the data for one post.
const [post, setPost] = useState(initialPost);
You don't need to use liked as a state because we can already access this information from the post data by seeing if our user is in the post.likes array or not. This allows us to have a "single source of truth" and we only need to make updates in one place.
const isLiked = post.likes.some((like) => like.id === user.id);
I'm confused about the likes array. It seems like an array of objects which are just {id: number}, in which case you should just have an array of ids of the users who liked the post. But maybe there are other properties in the object (like a username or timestamp).
When designing a component for something complex like a blog post, you want to break out little pieces that you can use in other places of your app. We can define a LikeButton that shows our heart. This is a "presentation" component that doesn't handle any logic. All it needs to know is whether the post isLiked and what to do onClick.
export const LikeButton = ({ isLiked, onClick }) => {
const Icon = isLiked ? FavoriteRoundedIcon: FavoriteBorderRoundedIcon;
return (
<Icon
style={{ color: isLiked ? "red" : "gray" }}
onClick={onClick}
/>
);
};
A lot of our logic regarding liking and unliking could potentially be broken out into some sort of usePostLike hook, but I haven't fully optimized this because I don't know what your API is doing and how we should respond to the response that we get.
When a user clicks the like button we want the changes to be reflected in the UI immediately, so we call setPost and add or remove the current user from the likes array. We have to set the state with a new object, so we copy all of the post properties that are not changing with the spread operator ...post and then override the likes property with an edited version. filter() and concat() are both safe array functions which return a new copy of the array.
We also need to call the API to post the changes. You are using the same url in both the "like" and "unlike" scenarios, so instead of calling axios.post and axios.delete, we can call the generalized function axios.request and pass the method name 'post' or 'delete' as an argument to the config object. [axios docs] We could probably combine our two setPost calls in a similar way and change likePost() and unlikePost() into one toggleLikePost() function. But for now, here's what I've got:
export const Post = ({ initialPost, user }) => {
const [post, setPost] = useState(initialPost);
const isLiked = post.likes.some((like) => like.id === user.id);
function likePost() {
console.log("liked the post");
// immediately update local state to reflect changes
setPost({
...post,
likes: post.likes.concat({ id: user.id })
});
// push changes to API
apiUpdateLike("post");
}
function unlikePost() {
console.log("unliked the post");
// immediately update local state to reflect changes
setPost({
...post,
likes: post.likes.filter((like) => like.id !== user.id)
});
// push changes to API
apiUpdateLike("delete");
}
// generalize like and unlike actions by passing method name 'post' or 'delete'
async function apiUpdateLike(method) {
try {
// send request to API
await axiosInstance.request("api/posts/" + post.slug + "/like/", { method });
// handle API response somehow, but not with window.location.reload()
} catch (e) {
console.log(e);
}
}
function onClickLike() {
if (isLiked) {
unlikePost();
} else {
likePost();
}
}
return (
<div>
<h2>{post.title}</h2>
<div>{post.likes.length} Likes</div>
<LikeButton onClick={onClickLike} isLiked={isLiked} />
</div>
);
};
CodeSandbox Link
this time its something general and simple so i have not really code to provide.
The general steps are:
Pass an array as a prop to a child component
Inside the child component loop over the array with v-for
now i call an axios post method to modify the "user-list" (the user list is the array)
vue should now update this array but it doesnt because a prop is not reactive.
The Main question is: How do i use computed properties as passed down props to have the array live updated?
here is some code though:
<div class="users" v-for="participant in part" :key="participant.id">
<template v-if="participant.name !== username">
{{participant.name}}
<span>
<a style="cursor:pointer" title="kick" #click="kickUser(participant)">
...
props: ["participants", "username", "channel"],
methods: {
kickUser(user) {
axios
.post("/kickuser", { user: user, channel: this.channel })
// .then((this.participants = []));
}
the kickuser axios post method removes a user from the db so the array is reduced by the user kicked
hope you can help me with the computed properties
Making some assumptions here so I'll probably need to edit this as information comes to light...
You have a parent component with data. For example
data: () => ({ participants: [...] }),
You then pass that data to a child component
<Child :participants="participants"
:username="..." :channel="..."/>
and within that child component, you perform an action that involves making an HTTP request.
What you do then is emit an event from the child component
methods: {
async kickUser (user) {
let { data } = await axios.post('/kickuser', { user, channel: this.channel })
this.$emit('kick', data)
}
}
and listen for this event in the parent
<Child :participants="participants" #kick="handleKick"
:username="..." :channel="..."/>
// parent "methods"
methods: {
handleKick (data) {
this.participants = [] // or whatever you need to do
}
}
This process is outlined in Vue's documentation ~ One-Way Data Flow
Basic scenario -
I have an input element for e-mail address. On click of save, the e-mail is saved into the database. When I refresh, I want the saved value from database to show up as the initial value of the input. After that, I want to control the input's value through regular component state.
The problem I am having is setting the initial state value from props.
I thought I can set the state from props in CDU by making a prevProps to currentProps check.
Actual scenario -
The props I am trying to compare is an array of objects. So CDU shallow comparison won't help. How do I go about doing this?
Please note that I am not using or do not want to use any form library as I just have 3 input fields in my applications.
Thanks
You need to get data from the database on the component mount and set the state of your email. Like this below example (I am doing it on edit user) -
componentDidMount() {
var headers = {
"Content-Type": "application/json",
Authorization: `Token ${authToken}`
};
axios
.get(`${serverURL}users/${this.props.match.params.id}`, {
headers: headers
})
.then(res => {
//console.log(res.data);
let userdetails = res.data;
this.setState({
first_name: userdetails.first_name,
last_name: userdetails.last_name,
email: userdetails.email,
});
})
.catch(function(error) {
console.log(error);
});
}
Possible solution for this:
1. Create local state for email e.g
state = {
email: '';
}
dispatch the action in componentDidMount which will fetch the data and store it in redux state
use static getDerivedStateFromProps(props, state) to get updated values from props (ref. link :https://reactjs.org/docs/react-component.html#static-getderivedstatefromprops) like
static getDerivedStateFromProps(nextProps, prevState) {
// do things with nextProps.email and prevState.email
return {
email: nextProps.someProp,
};
}
I'm trying to get a good understanding of VueJS, and I'm using it with Laravel 5.7 for a personal project, but I can't exactly figure out how to do a, probably, simple task a "like" button\icon.
So, here's the situation, I have a page, displaying various posts from my database, and at the bottom of each post I want a "like toogle" button, which I made with an icon followed by the number of likes on that post; At first the button will contain the data retrieved from the corresponding database table, but if you click it will increase the displayed number by one and insert a new like in the database.
I made the "like" icon as a component :
<section class="bottomInfo">
<p>
<likes now="{{ $article->likes }}"></likes>
<span class="spacer"></span>
<span class="entypo-chat">
{{ $article->comments }}
</p>
</section> <!-- end .bottomInfo -->
As you can see there's a <likes> in which I added a prop now, by what I'm understanding till now about components, in this way I can insert the data from my db as a starting value (now contains the db row value), problem is, I don't know where\how to keep that value in my app, in which I'm gonna also use axios for increasing the likes.
Here's the component:
<template>
<span class="entypo-heart"> {{ now }}</span>
</template>
<script>
export default {
props: ['now'],
data() {
return {
like: this.now
}
},
mounted() {
console.log('Component mounted.');
}
}
</script>
What I tried to do (and I don't know if it's correct) is to pass the value of now to the data function inside a property named like, so, if I understood correctly, that variable like is now part of my properties in my main Vue instance, which is this one
const app = new Vue({
el: '#main',
mounted () {
console.log("The value of 'like' property is " + this.like)
},
methods: {
toggleLike: function() {
} //end toggleLike
}
});
The mounted function should print that property value, but instead I get
The value of 'like' property is undefined
Why? Is this how it works? How can I make it so I can get that value and also update it if clicked, to then do a request to my API? (I mean, I'm not asking how to do those single tasks, just where\how to implement it in this situation). Am i getting the component logic right?
Probably a bit more verbosity never hurt:
props: {
now: {
type: Number,
required: true
}
}
Instead of using the data function, use a computed property:
computed: {
likes: {
get: function() {
return this.now
}
}
}
However, here comes the problem.
If you need to change the # of likes after the user clicks like, you have to update this.now. But you can't! It's a property, and properties are pure. Vue will complain about mutating a property
So now you can introduce a data variable to determine if the user has clicked that like button:
data() {
return {
liked: 0
}
}
Now we can update our computed property:
likes: {
get: function() {
return this.now + this.liked
}
}
However, what are we liking? Now we need another property:
props: {
id: {
type: Number,
required: true
},
now: {
type: Number,
required: true
}
}
And we add a method:
methods: {
add: function() {
//axios?
axios.post(`/api/articles/${this.id}/like`)
.then (response => {
// now we can update our `liked` proper
this.liked = 1
}).catch(error => {
// handle errors if you need to
)}
}
}
And, let's make sure clicking our heart fires that event:
<span class="entypo-heart" #click="add"> {{ now }}</span>
Finally our likes component requires an id property from our article:
<likes now="{{ $article->likes }}" id="{{ $article->id }}"></likes>
With all this in place; you're a wizard now, Harry.
Edit
It should be noted that a user will be forever able to like this, over and over again. So you need some checks in the click function to determine if they like it. You also need a new prop or computed property to determine if it was already liked. This isn't the full monty yet.
I am making a call to Firebase, to check if user has a property.
If he has, I want him to be redirected to page A, else I want to redirect him to page B.
The problem is I think Firebase takes to long to come back, so React just renders the component with the default value.
My code:
export default class CheckIfHasCurrentTasksComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
questionId: this.props.match.params.id
};
}
async componentDidMount() {
var uid = firebase.auth().currentUser.uid;
var AssignedWordRef = await firebase.database().ref('Users').child(uid).child('assignedWork');
await AssignedWordRef.on('value', snapshot => {
var question = snapshot.val();
console.log('question', question);
if (question.length > 0) {
this.setState({
questionId: question,
});
console.log('redirect', this.state.questionId);
}
else {
this.setState({
questionId: this.props.match.params.id,
});
console.log('No redirect', this.state.questionId);
}
})
}
render() {
console.log('questionId', this.state.questionId);
return <QuestionComponent questionId={this.state.questionId}/>
}
}
The logs:
questionId -LEU6zUnLTO84KGh3pua CheckIfHasCurrentTasksComponent.js:35
question -LEU1fr2TmxHFKD3ObG_ CheckIfHasCurrentTasksComponent.js:18
questionId -LEU1fr2TmxHFKD3ObG_ CheckIfHasCurrentTasksComponent.js:35
redirect -LEU1fr2TmxHFKD3ObG_ CheckIfHasCurrentTasksComponent.js:23
The code seems to be running twice for some reason. The id is the one coming from a previous component. The last 3 are the ones from the database.
The confusing part is that they get logged async-like. How can I make it synchronous/wait for the response from Firebase to come back?
The this.props.match.params.id is an id coming from a previous component. This one is acting all right.
The confusing part is that they get logged async-like. How can I make it synchronous/wait for the response from Firebase to come back?
There's an approach where you can make it look synchronous.
You can attach a variable to this class, which by default, is false. Once, you have received the response, you can set that variable to true.
For instance -
this.state.isQuestionIDAvailable=false;
And in your firebase response, ( do take care of this's scope when using it )
this.setState({
isQuestionIDAvailable: true
});
You can then attach a condition to your component like -
{this.state.isQuestionIDAvailable && <QuestionComponent />}
Also, if <QuestionComponent/> is the only thing you are rendering you'll not need the curly braces as well. You can write it as
return this.state.isQuestionIDAvailable && <QuestionComponent />
Hope this answers :)