Button not changing state in react js based on api call - javascript

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

Related

Vue & Vuex not retrieving proper value in html

I have a navbar where I only show certain menu items based off the user's role.
Here is the HTML (I removed all the menu items except one to simplify):
<v-speed-dial class="speed-dial-container contextual-text-menu" v-if="user && user.emailVerified" fixed top right
direction="bottom" transition="slide-y-transition">
<v-icon v-if="checkAuthorization(['superAdmin', 'admin', 'pastor', 'member']) === true" class="mt-2"
#click="sendComponent({ component: 'Dashboard' })">$admin</v-icon>
</v-speed-dial>
My method:
async checkAuthorization(permissions) {
if (permissions.length > 0) {
const temp = await this.$store.dispatch('UserData/isAuthorized', permissions)
return temp
}
},
Vuex store:
isAuthorized({
state
}, permissions) {
const userRoles = state.roles
if (permissions && userRoles) {
const found = userRoles.some(r => permissions.includes(r))
return found
}
},
All of my console logs show the correct values but the HTML is not responding accordingly.
Example: in this line of code checkAuthorization(['superAdmin', 'admin', 'pastor', 'member']) === true I added 'member' and I am logged in as a user that ONLY has the 'member' role. When looking through the console logs everything returns true so I should see this menu item but it does not show.
As someone pointed out in the comments, checkAuthorization is an async function and will return a Promise, so you cannot check for promise === true.
That aside, I would change isAuthorized to be a vuex getter and not an action, e.g.
getters: {
// ...
isAuthorized: (state) => (permissions) => {
if (permissions && state.roles) {
return state.roles.some(r => permissions.includes(r))
}
return false;
}
}
And update checkAuthorization to not return a promise e.g.
function checkAuthorization(permissions) {
if (permissions.length > 0) {
return this.$store.getters.isAuthorized(permissions);
}
return false;
}
What I usually do :
I add another user state as Unknown and make it the default state.
In main main.js (or main.ts) I call state.initialize(), which determines user's state.
And, the key thing is to use navigation guards. Instead of checking routing guards on router-link (or url or anywhere in this step), you should define it in the router. There is a router.beforeEach function you can use, so that you can check if user is authorized to use that route, and redirect the user to 401 page if the user don't have permission.
https://router.vuejs.org/guide/advanced/navigation-guards.html

react.js how to display multiple error messages

I just got started to react so please bear with me. I don't know exactly what I am doing, I'm just picking those things as I go so I'll do my best to walk you through my mental process when building this.
My intentions are to create a registration component, where the backend returns the validation errors in case there are any in form of an object which has following structure.
{
"username": [
"A user with that username already exists."
],
"email": [
"A user is already registered with this e-mail address."
]
}
The state manager that I chose to be using is redux, so this comes back every time when the register function is dispatched.
Since it has this structure I wrote a function to help me decompose it and pick up only on the actual errors (the strings).
const walkNestedObject = (obj, fn) => {
const values = Object.values(obj)
values.forEach(val =>
val && typeof val === "object" ? walkNestedObject(val, fn) : fn(val))
}
now I want to display them in the view, so I wrote another function which is supposed to do that
const writeError = (value) => {
return <Alert message={value} type="error" showIcon />
}
Down in the actual component I am calling it as this:
{(props.error) ? walkNestedObject(props.error, writeError) : null}
To my surprise if I console.log the value above return in writeError it works flawlessly, every single error gets printed, but none of them gets rendered.
To debug this I've tried multiple variations and none of them seemed to work, I even called the writeError function in the component as
{writeError('test')}
and it worked for some reason.
At this stage I'm just assuming there's some react knowledge required to fulfil this task that Im just now aware of.
EDIT:
A mock example can be found over here
Also, I've tried using the first two answers and when mapping through the errors I get this
Unhandled Rejection (TypeError): props.error.map is not a function
with other variations, it mentions the promise from so I'd include how I manage the API request
export const authSignup = (username, email, password1, password2) => dispatch => {
dispatch(authStart());
axios.post('http://127.0.0.1:8000/rest-auth/registration/', {
username: username,
email: email,
password1: password1,
password2: password2
})
.then(res => {
const token = res.data.key;
const expirationDate = new Date(new Date().getTime() + 3600 * 1000);
localStorage.setItem('token', token);
localStorage.setItem('expirationDate', expirationDate);
dispatch(authSuccess(token));
dispatch(checkAuthTimeout(3600));
})
.catch(err => {
dispatch(authFail(err.response.data))
})
}
Consider changing the topology of your error messages:
"errors": [
{ "type": "username", "message": "Username already in use." },
{ "type": "email", "message": "Email address already in use."}
]
That makes your implementation a bit easier:
// MyLogin.jsx
import React from 'react'
const MyLogin = () => {
/**
* Here we're using state hooks, since it's much simpler than using Redux.
* Since we don't need this data to be made globally available in our
* application, it doesn't make sense to use Redux anyway.
*/
const [errors, setErrors] = React.useState([])
const handleLogin = (event) => {
event.preventDefault()
axios.post('/api/login', formData).then(() => successAction(), (error: any) => {
setErrors(error) // Update our local state with the server errors
})
}
return (
<>
{errors.length && ( // Conditionally render our errors
errors.map((error) => (
<Alert type={error.type} message={error.message} />
)
)}
<form onSubmit={handleLogin}>
<input type='text' name='email' />
<input type='text' name='username' />
<input type='password' name='password' />
</form>
<>
)
}
export default MyLogin
Your walkNestedFunction function checks each layer of an object, and if a given layer of the object is an object itself, it then uses that object to run your function - which in this case is writeError. writeError returns an error <Alert /> as soon as an error arises. But when you stick writeError inside the circular logic of walkNestedFunction, it will hit the first return statement, render it to the page, and then stop rendering. I think this is why you're getting the complete list of errors logged to the console. Your walkNestedFunction continues cycling down through object layers until its done. But in React, only the first return statement will actually render.
A better tactic would be to modify your writeError function to record the erors to a state variable. Then you can render this state variable. Every time the state is updated, the component will rerender with the updated state.
// Define state in your component to contain an array of errors:
state = {
errors: []
}
// Add an error into state with each iteration of writeError
const writeError = (error) => {
this.setState({
errors: [
...this.state.errors,
error
]
})
}
// inside render(), render the state variable containing your errors
<div>
{ this.state.errors.map(error => <p>error</p>) }
</div>
`

Render React component using Firestore data

I'm trying to render my Guild component with data from Firestore. I put the data from Firestore into my state as an array, then when I call the component and try to render it, nothing shows. I want to believe I'm doing something very wrong here (haven't been working with React for very long), but I'm not getting any errors or warnings, so I'm not sure exactly what's happening.
Guilds.js
<Col>
<Card>
<CardBody>
<CardTitle className={this.props.guildFaction}>{this.props.guildName}</CardTitle>
<CardSubtitle>{this.props.guildServer}</CardSubtitle>
<CardText>{this.props.guildDesc}</CardText>
</CardBody>
</Card>
</Col>
Render function
renderCards() {
var guildComp = this.state.guilds.map(guild => {
console.log(guild)
return <Guilds
key={guild.id}
guildFaction={guild.guildFaction}
guildServer={guild.guildServer}
guildName={guild.guildName}
guildDesc={guild.guildDesc} />
})
return <CardDeck>{guildComp}</CardDeck>
}
Fetching Firestore Data
guildInfo() {
Fire.firestore().collection('guilds')
.get().then(snapshot => {
snapshot.forEach(doc => {
this.setState({
guilds: [{
id: doc.id,
guildDesc: doc.data().guildDesc,
guildFaction: doc.data().guildFaction,
guildName: doc.data().guildName,
guildRegion: doc.data().guildRegion,
guildServer: doc.data().guildServer
}]
})
console.log(doc.data().guildName)
})
})
}
UPDATE: solved, fix is in the render function.
Well, you using state "guilds" but you update state "posts" or I miss something?
I see few things here:
your component is Guild.js, but you are rendering <Guilds />
You are setting state to posts, but using this.state.guilds to render the components
You are overriding that piece of state each time to the last object in the snapshot, with the way you are mapping the Firestore data
you are setting the ids in the list wrong using doc.id instead of doc.data().id
You aren't mapping guilds to render. guilds is an array of guild objects, so you should do something like guilds.map(guild => { return <Guild /> }
These are few things to fix, and then try to console.log(this.state.guilds) before rendering and see if you get the right data
I think your issue is that because setState is async, by the time it actually sets the state doc is no longer defined. Try creating the array first, then call setState outside of the loop ie:
guildInfo() {
Fire.firestore().collection('guilds')
.get().then(snapshot => {
let guilds = []
snapshot.forEach(doc => {
guilds.push({
id: doc.id,
guildDesc: doc.data().guildDesc,
guildFaction: doc.data().guildFaction,
guildName: doc.data().guildName,
guildRegion: doc.data().guildRegion,
guildServer: doc.data().guildServer
});
})
this.setState({guilds});
})
}
Try to use a map function, and in the callback function of the setState, try to console log your state after the update:
guildInfo() {
Fire.firestore().collection('guilds')
.get()
.then(snapshot => {
const guilds = snapshot.map(doc => {
return {
id: doc.id,
guildDesc: doc.data().guildDesc,
guildFaction: doc.data().guildFaction,
guildName: doc.data().guildName,
guildRegion: doc.data().guildRegion,
guildServer: doc.data().guildServer
};
this.setState({guilds}, () => console.log(this.state))
})
})
})
}
If in the console log there's a little [i] symbol near your state, it means that the state is not ready, and therefore it's am async issue. Replacing the forEach with the map function may already help though.

NgRx Store - conditionally loading data based on property of another state object

I have an app that renders fixtures, results etc from my API based on a season id - this id is stored in state as a property of SeasonState:
export interface SeasonsState extends EntityState<Season> {
allSeasonsLoaded: boolean;
currentlySelectedSeasonId: number;
}
This is used by other components to determine which fixtures, results etc to fetch from the API and store in state. For example:
this.store
.pipe(
select(selectCurrentlySelectedSeason)
).subscribe(seasonId => {
this.store.dispatch(new AllFixturesBySeasonRequested({seasonId}));
this.fixtures$ = this.store
.pipe(
select(selectAllFixturesFromSeason(seasonId))
);
});
This works well, but what I'd really like is to be able to only fetch fixtures again fixtures for that particular season are not already stored in state.
I've tried creating a selector to use to conditionally load the data from the API in my effects:
export const selectSeasonsLoaded = (seasonId: any) => createSelector(
selectFixturesState,
fixturesState => fixturesState.seasonsLoaded.find(seasonId)
);
But I am unsure how to implement this / whether this is the right approach.
EDIT: using info from the answer below, I have written the following Effect, however see the comment - I need to be able to use seasonId from the payload in my withLatestFrom.
#Effect()
loadFixturesBySeason$ = this.actions$
.pipe(
ofType<AllFixturesBySeasonRequested>(FixtureActionTypes.AllFixturesBySeasonRequested),
withLatestFrom(this.store.select(selectAllFixtures)), // needs to be bySeasonId
switchMap(([action, fixtures]) => {
if (fixtures.length) {
return [];
}
return this.fixtureService.getFixturesBySeason(action.payload.seasonId);
}),
map(fixtures => new AllFixturesBySeasonLoaded({fixtures}))
);
Have your effect setup like this [I am using ngrx 6 so tested on ngrx 6; If you are using some other version then you will get an idea and adjust the code accordingly] -
#Effect() allFixturesBySeasonRequested: Observable<Action> =
this._actions$
.pipe(
//Please use your action here;
ofType(actions.AllFixturesBySeasonRequested),
//please adjust your action payload here as per your code
//bottom line is to map your dispatched action to the action's payload
map(action => action.payload ),
switchMap(seasonId => {
//first get the fixtures for the seasonId from the store
//check its value if there are fixtures for the specified seasonId
//then dont fetch it from the server; If NO fixtures then fetch the same from the server
return this.store
.pipe(
select(selectAllFixturesFromSeason(seasonId)),
//this will ensure not to trigger this again when you update the fixtures in your store after fetching from the backend.
take(1),
mergeMap(fixtures => {
//check here if fixtures has something OR have your logic to know
//if fixtures are there
//I am assuming it is an array
if (fixtures && fixtures.lenght) {
//here you can either return NO action or return an action
//which informs that fixtures already there
//or send action as per your app logic
return [];
} else {
//NO fixtures in the store for seasonId; get it from there server
return this.http.get(/*your URL to get the fixtures from the backend*/)=
.pipe(
mergeMap(res => {
return [new YourFixtureFetchedSucccess()];
}
)
)
}
})
);
})
)
Now you need to dispatch the action which fetches the fixtures for the specified seasonId from your service/component or the way your app is designed.
Hope it will give you an idea and helps in solving your problem.

How to fetch and populate data from firebase collections in react correctly?

So i have component called Agents, that's by it's nature is table with rows that contain data about agents.
I want to get full data about agents from firestore, populate it with data from another collection in firestore, and then render it to the screen.
Here is working piece of code, but i have some questions:
Why now i have render with some delay? (First i have spinner about loading - it's okay, i expect it, but then spinner hides and i see 1 agent in table for 0.5s, and then i see all data. I want to see all data after spinner gone. What is the problem of my code?
How to populate data. Maybe there is another way to do that, or maybe there is another good practice, cause i think my "componentDidMount" is horrible, the solutions that i use are not common i guess.
So for 2 question i will describe what i want:
In users collection i have documents, they are objects, that have property called - profile. That property contains ID of document from collection called roles! I want to get that data by that document id. That's why my componendDidMount is async, cause i want to wait till i get data from roles collection by id of document.
Help me please optimise my code )
class Agents extends Component {
state = {
isLoading: true,
agents: [],
showOptionsFor: null
}
async componentDidMount() {
firebase
.firestore()
.collection('users')
.orderBy('lastName')
.onSnapshot(async snapshot => {
let changes = snapshot.docChanges()
let agents = this.state.agents
for (const change of changes) {
const agent = {
id: change.doc.id,
...change.doc.data()
}
if (change.type === 'added' || change.type === 'modified') {
await firebase
.firestore()
.collection('roles')
.doc(change.doc.data().profile).get().then( response => {
agent['role'] = response.data().profile
agents.push(agent)
}
)
}
if (change.type === 'modified') {
const newAgentsArray = agents.filter(element => {
return element.id !== change.doc.id
})
agents = newAgentsArray
agents.push(agent)
}
}
this.setState({
agents: agents,
isLoading: false
})
})
}

Categories