Can't set state in a onClick function in React Hook - reactjs

Hi i cant set state in a using a function i defined for onClick. All other lines are working except setting the state.
export default function SaleProducts(props) {
const [currentSelected, setSelected] = useState(props.location.state.pid);
useEffect(() => {
superagent
.post("url")
.set("Content-Type","application/x-www-form-urlencoded")
.send({"access_token":token})
.set('accept', 'json')
.end((error, response) => {
if(error){
console.log("Error")
}
else{
var json = response.body;
json.products.map((res) => {
var array = [res.title,"Not yet published",res.variants[0].price,<Button onClick={(event) => handleItemDeletion(event,res.id)}>Delete Item</Button>];
arr.push(array);
})
,[currentSelected]}
const handleItemDeletion = (event,id) =>{
event.preventDefault();
var cSelected = currentSelected.replace(id,'');
setSelected((currentSelected) => cSelected); //this is not working
console.log("Current Selected : ",currentSelected)
}
return(<arr>); //this is only for representation
OnClick function is getting called but only that setSelected line is not working. The state is not changing it is still like before.

You should pass value to setSelected, not a function. Something like this: setSelected(cSelected);

Setting the state is not correct. Try this,
export default function SaleProducts(props) {
const [currentSelected, setSelected] = useState(props.location.state.pid);
useEffect(() => {
superagent
.post("url")
.set("Content-Type","application/x-www-form-urlencoded")
.send({"access_token":token})
.set('accept', 'json')
.end((error, response) => {
if(error){
console.log("Error")
}
else{
var json = response.body;
json.products.map((res) => {
var array = [res.title,"Not yet published",res.variants[0].price,<Button onClick={(event) => handleItemDeletion(event,res.id)}>Delete Item</Button>];
arr.push(array);
})
,[currentSelected]}
const handleItemDeletion = (event,id) =>{
event.preventDefault();
var cSelected = currentSelected.replace(id,'');
setSelected(cSelected); // check here
console.log("Current Selected : ",currentSelected)
}
return(<arr>);

Related

Cannot setstate in nested axios post request in react

I am trying to access the res.data.id from a nested axios.post call and assign it to 'activeId' variable. I am calling the handleSaveAll() function on a button Click event. When the button is clicked, When I console the 'res.data.Id', its returning the value properly, but when I console the 'activeId', it's returning null, which means the 'res.data.id' cannot be assigned. Does anyone have a solution? Thanks in advance
const [activeId, setActiveId] = useState(null);
useEffect(() => {}, [activeId]);
const save1 = () => {
axios.get(api1, getDefaultHeaders())
.then(() => {
const data = {item1: item1,};
axios.post(api2, data, getDefaultHeaders()).then((res) => {
setActiveId(res.data.id);
console.log(res.data.id); // result: e.g. 10
});
});
};
const save2 = () => {
console.log(activeId); // result: null
};
const handleSaveAll = () => {
save1();
save2();
console.log(activeId); // result: again its still null
};
return (
<button type='submit' onClick={handleSaveAll}>Save</button>
);
Setting the state in React acts like an async function.
Meaning that the when you set the state and put a console.log right after it, like in your example, the console.log function runs before the state has actually finished updating.
Which is why we have useEffect, a built-in React hook that activates a callback when one of it's dependencies have changed.
Example:
useEffect(() => {
console.log(activeId);
}, [activeId);
The callback will run every time the state value changes and only after it has finished changing and a render has occurred.
Edit:
Based on the discussion in the comments.
const handleSaveSections = () => {
// ... Your logic with the `setState` at the end.
}
useEffect(() => {
if (activeId === null) {
return;
}
save2(); // ( or any other function / logic you need )
}, [activeId]);
return (
<button onClick={handleSaveSections}>Click me!</button>
)
As the setState is a async task, you will not see the changes directly.
If you want to see the changes after the axios call, you can use the following code :
axios.post(api2, data, getDefaultHeaders())
.then((res) => {
setActiveId(res.data.id)
console.log(res.data.id) // result: e.g. 10
setTimeout(()=>console.log(activeId),0);
})
useEffect(() => {
}, [activeId]);
const [activeId, setActiveId] = useState(null);
const save1 = () => {
const handleSaveSections = async () => {
activeMetric &&
axios.get(api1, getDefaultHeaders()).then(res => {
if (res.data.length > 0) {
Swal.fire({
text: 'Record already exists',
icon: 'error',
});
return false;
}
else {
const data = {
item1: item1,
item2: item2
}
axios.post(api2, data, getDefaultHeaders())
.then((res) => {
setActiveId(res.data.id)
console.log(res.data.id) // result: e.g. 10
})
}
});
}
handleSaveSections()
}
const save2 = () => {
console.log(activeId); //correct result would be shown here
}
const handleSaveAll = () => {
save1();
save2();
}
return (
<button type="submit" onClick={handleSaveAll}>Save</button>
)

useEffect function inside context unaware of state changes inside itself

I am building a messaging feature using socket.io and react context;
I created a context to hold the conversations that are initially loaded from the server as the user passes authentication.
export const ConversationsContext = createContext();
export const ConversationsContextProvider = ({ children }) => {
const { user } = useUser();
const [conversations, setConversations] = useState([]);
const { socket } = useContext(MessagesSocketContext);
useEffect(() => {
console.log(conversations);
}, [conversations]);
useEffect(() => {
if (!socket) return;
socket.on("userConversations", (uc) => {
let ucc = uc.map((c) => ({
...c,
participant: c.participants.filter((p) => p._id != user._id)[0],
}));
setConversations([...ucc]);
});
socket.on("receive-message", (message) => {
console.log([...conversations]);
console.log(message);
setConversations((convs) => {
let convIndex = convs.findIndex(
(c) => c._id === message.conversation._id
);
let conv = convs[convIndex];
convs.splice(convIndex, 1);
conv.messages.unshift(message);
return [conv, ...convs];
});
});
}, [socket]);
return (
<ConversationsContext.Provider
value={{
conversations,
setConversations,
}}
>
{children}
</ConversationsContext.Provider>
);
};
The conversations state is updated with the values that come from the server, and I have confirmed that on the first render, the values are indeed there.
Whenever i am geting a message, when the socket.on("receive-message", ...) function is called, the conversations state always return as []. When checking devTools if that is the case I see the values present, meaning the the socket.on is not updated with the conversations state.
I would appreciate any advice on this as I`m dealing with this for the past 3 days.
Thanks.
You can take "receive-message" function outside of the useEffect hook and use thr reference as so:
const onReceiveMessageRef = useRef();
onReceiveMessageRef.current = (message) => {
console.log([...conversations]);
console.log(message);
setConversations((convs) => {
let convIndex = convs.findIndex(
(c) => c._id === message.conversation._id
);
let conv = convs[convIndex];
convs.splice(convIndex, 1);
conv.messages.unshift(message);
return [conv, ...convs];
});
};
useEffect(() => {
if (!socket) return;
socket.on("userConversations", (uc) => {
let ucc = uc.map((c) => ({
...c,
participant: c.participants.filter((p) => p._id != user._id)[0],
}));
setConversations([...ucc]);
});
socket.on("receive-message", (...r) => onReceiveMessageRef.current(...r));
}, [socket]);
let me know if this solves your problem

Why does React state return undefined but page still loads from state OK?

I am attempting to develop a React app which makes a call to a database to load a set of pages to a board to build a drag and drop decision tree.
I am only just starting out with React, so keen to hear about anything I'm doing wrong here.
Using 'useEffect' the pageTree function will load the pages up on the first load and on every refresh, however the pages state returns with an empty array instead of the current pages.
Strangely enough the pages all show up on the board with the pages.map function which works on the pages state... (which returns as empty on console.log...)
If I add a page to the array it saves the change to the database, but then will only show the new page on the board. You will then have to refresh to see the new set of pages (including the added page).
Calls to add or delete a page are called by the layout menu buttons in the parent component.
Console after refresh
Additionally, if I move a page, the state will console OK:
Page state in console after moving a page. DB call and state update works OK
function PageTree({AddNewPageFunc}) {
const [pages, setPages] = useState([]);
const movePage = useCallback((droppedPage) => {
const updatedPages = pages.map(page => droppedPage._id == page._id ? droppedPage : page);
setPages(updatedPages);
}, [pages]);
const [{isOver}, drop] = useDrop(() => ({
accept: ItemTypes.PAGECARD,
drop(page, monitor) {
const delta = monitor.getDifferenceFromInitialOffset();
let x = Math.round(page.x + delta.x);
let y = Math.round(page.y + delta.y);
page.x = x;
page.y = y;
movePage(page);
setNewPagePosition(page);
return undefined;
},
}), [movePage]);
const setNewPagePosition = async (pageDetails) => {
console.log("function called to update page position");
Api.withToken().post('/pageupdate/'+pageDetails._id,
pageDetails
).then(function (response) {
console.log("moved page: ",response.data)
}).catch(function (error) {
//console.log(error);
});
}
React.useEffect(() => {
AddNewPageFunc.current = AddNewPage
}, [])
const AddNewPage = useCallback(() => {
console.log("calling add new page function")
console.log("the pages before the API call are ",pages)
Api.withToken().post('/addblankpage/'
).then(function (response) {
console.log("produced: ",response.data);
setPages(pages.concat(response.data))
console.log("the pages after updating state are: ",pages)
}).catch(function (error) {
//console.log(error);
});
}, [pages]);
const handleDelete = async (id) => {
Api.withToken().post('/deletepages/'+id
).then(function (response) {
let index = pages.findIndex(function(item){
return item.id === response.data._id;
});
const PageRemoved = pages.splice(index, 1);
setPages(PageRemoved);
}).catch(function (error) {
//console.log(error);
});
}
useEffect(() => {
Api.withToken().get('/pages/')
.then(res => {
setPages(res.data);
console.log('res data ',res.data);
console.log('pages ',pages);
})
}, []);
return (
<div ref={drop} style={styles}>
{pages.map((page) => (<PageCard page={page} id={page._id} key={page._id} handleDelete={() => handleDelete(page._id)} handleMaximise={() => handleMaximise(page)} handleCopy={() => handleCopy(page)}/>))}
</div>
)
}
export default PageTree;
As Danielprabhakaran pointed out, the issue was the callback in React.useEffect. On adding a new page it needed to send the updated page state back to the parent component.
Using console.log on a state after an API call seems to be fraught, even if using .then(console.log(state)
function PageTree({AddNewPageFunc}) {
const [pages, setPages] = useState([]);
const movePage = useCallback((droppedPage) => {
const updatedPages = pages.map(page => droppedPage._id == page._id ? droppedPage : page);
console.log("updated pages ",updatedPages);
setPages(updatedPages);
console.log("set pages ",pages);
}, [pages]);
const [{isOver}, drop] = useDrop(() => ({
accept: ItemTypes.PAGECARD,
drop(page, monitor) {
const delta = monitor.getDifferenceFromInitialOffset();
let x = Math.round(page.x + delta.x);
let y = Math.round(page.y + delta.y);
page.x = x;
page.y = y;
movePage(page);
setNewPagePosition(page);
return undefined;
},
}), [movePage]);
const setNewPagePosition = async (pageDetails) => {
console.log("function called to update page position");
Api.withToken().post('/pageupdate/'+pageDetails._id,
pageDetails
).then(function (response) {
console.log("?worked ",response)
}).catch(function (error) {
//console.log(error);
});
}
React.useEffect(() => {
AddNewPageFunc.current = AddNewPage
}, [pages])
const AddNewPage = useCallback(() => {
console.log("calling add new page function")
console.log("the pages before the API call are ",pages)
Api.withToken().post('/addblankpage/'
).then(function (response) {
console.log("produced: ",response.data);
setPages(pages.concat(response.data))
console.log("the pages after updating state are: ",pages)
}).catch(function (error) {
//console.log(error);
});
}, [pages]);
const handleDeletedCallback = (deletedIndex) => {
console.log("delete callback fired")
setPages(pages.splice(deletedIndex, 1));
}
useEffect(() => {
Api.withToken().get('/pages/') //can add in a prop to return only a given tree once the app gets bigger
.then(res => {
setPages(res.data);
console.log('res data ',res.data);
console.log('pages ',pages);
})
}, []);
return (
<div ref={drop} style={styles}>
{pages.map((page, index) => (<PageCard page={page} id={page._id} key={page._id} index={index} deleteCallback={handleDeletedCallback} handleMaximise={() => handleMaximise(page)} handleCopy={() => handleCopy(page)}/>))}
</div>
)
}
export default PageTree;

Why is UI not updating after button click in React?

I'm working on something similar like Youtube like/dislike functionality. First getting result by it's id. Then checking and filtering and lastly doing put request to the database. But the problem is that 'likes' values in the UI changes only after page refresh. I tried using useState hook and manipulate the state when getting response from put request, without succeeding. Everything happens on button click.
Any advice is strongly appreciated.
My States
const [likes, setLikes] = useState([])
const [dislikes, setDislikes] = useState([])
Getting the review
const getDislikedReviewById = (id) => {
axios.get(`https://landlordstrapi.herokuapp.com/cool-project/${id}`)
.then((response) => {
handleDislike(response.data)
})
.catch(err => {
console.log(err);
});
}
Working with functionality
const handleDislike = (data) => {
const id = data.id
setDislikes([...data.comment_info.dislikes])
setLikes([...data.comment_info.likes])
const userToken = localStorage.getItem('user') || []
const checkIfLikeExists = likes.find(item => item === userToken)
const checkIfDislikeExists = dislikes.find(item => item === userToken)
if(checkIfLikeExists && checkIfLikeExists.length) {
setLikes(likes.filter(a => a !== userToken))
setDislikes(dislikes.push(userToken))
}
if(!checkIfDislikeExists && !checkIfLikeExists) {
setDislikes(dislikes.push(userToken))
}
JSON.stringify(dislikes)
updateDislikes(data, id, dislikes, likes)
}
When everything is done sending UPDATE request
const updateDislikes = (data, id, dislikes, likes) => {
axios.put(`https://landlordstrapi.herokuapp.com/cool-project/${id}`, {
comment_info: {
likes: likes,
dislikes: dislikes,
comment: data.comment_info.comment
}
})
.then(function(response){
console.log('saved successfully')
});
}
My like button
<Votes>
<GrLike style={{cursor: "pointer"}} onClick={() => getReviewById(id)} size={10}/>
<VoteValue>
{likes && likes.length ? likes.length : null}
</VoteValue>
</Votes>
Not sure if you want to update the state or set the data returned by the api. I assumed now you would like to set the data from the api. If you want to set the state you can do so by using the old state, so pass a function to the setState call
setDislikes((oldDislikes) => [...oldDislikes, userToken]);
and I would update the handleDislike function as so
const handleDislike = (data) => {
const {
id,
comment_info: { dislikes, likes }
} = data;
let newLikes = likes;
let newDislikes = dislikes;
const userToken = localStorage.getItem("user") || [];
const likeExists = likes.find((item) => item === userToken);
const dislikeExists = dislikes.find((item) => item === userToken);
if (likeExists) {
newLikes = newLikes.filter((a) => a !== userToken);
}
if ((!dislikeExists && !likeExists) || likeExists) {
newDislikes = [...dislikes, userToken];
}
// set new values
setDislikes(newDislikes);
setLikes(newLikes);
updateDislikes(data, id, newDislikes, newLikes);
};

how to handle race conditions in class components?

Suppose there is a component where ask server to do some search and response will be rendered. How to ensure most recent request's response is rendered even if server side for any reason answers in different ordering? I'm not asking about cancelling previous request since it's not always possible with reasonable efforts.
onClick = () => {
apiCall(this.state.searchQuery).then(items => this.setState({ items }));
};
Is there elegant way to handle that? By now I know few approaches:
disabling button till request comes(provides bad experiences in large amount of cases - say for searching while typing)
checking inside then() if request's params matches this.props/this.state data(does not handle case when we intentionally forced new search with same query - say by pressing Enter/clicking "Search" button)
onClick = () => {
const searchQuery = this.state.searchQuery;
apiCall(searchQuery)
.then(items =>
this.state.searchQuery === searchQuery
&& this.setState({ items })
);
};
marking requests somehow and checking if it's latest(works, but looks too verboose especially if there are few requests we need to check)
searchQueryIndex = 0;
onClick = () => {
this.searchQueryIndex++;
const index = this.searchQueryIndex;
apiCall(this.state.searchQuery)
.then(items =>
this.searchQueryIndex === searchQueryIndex
&& this.setState({ items })
);
};
I'd call that trio "ugly, broken and messy".
Is there something such clear way as hooks allow:
useEffect(() => {
const isCanceled = false;
apiCall(searchQuery).then(items => !isCanceled && setItems(items));
return () => {isCanceled = true;};
}, [searchQuery])
Your onClick handler suggest a class component since you use this and this.setState:
onClick = () => {
apiCall(this.state.searchQuery).then(items =>
this.setState({ items })
);
};
I adjusted onlyLastRequestedPromise to take a function that will return something (you can return Promise.reject('cancelled') or anything).
const onlyLastRequestedPromise = (promiseIds => {
const whenResolve = (
promise,
id,
promiseID,
resolveValue,
whenCancelled = () => Promise.reject('cancelled')
) => {
if (promise !== undefined) {
//called by user adding a promise
promiseIds[id] = {};
} else {
//called because promise is resolved
return promiseID === promiseIds[id]
? Promise.resolve(resolveValue)
: whenCancelled(resolveValue);
}
return (function(currentPromiseID) {
return promise.then(function(result) {
return whenResolve(
undefined,
id,
currentPromiseID,
result
);
});
})(promiseIds[id]);
};
return (id = 'general', whenCancelled) => promise =>
whenResolve(
promise,
id,
undefined,
undefined,
whenCancelled
);
})({});
A class example on how to use it:
class Component extends React.Component {
CANCELLED = {};
last = onlyLastRequestedPromise(
'search',
() => this.CANCELLED
);
onSearch = () => {
this.last(apiCall(this.state.searchQuery)).then(
items =>
items !== this.CANCELLED && this.setState({ items })
);
};
changeAndSearch = e => {
this.setState(
{}, //state with new value
() => this.onSearch() //onSearch after state update
);
};
render() {
return (
<div>
<SearchButton onClick={this.onSearch} />
<Other onChange={this.changeAndSearch} />
</div>
);
}
}
I agree it's a lot of code but since you put most of the implementation in the lib it should not clutter your components.
If you had a functional component you could create the last function with useRef:
//
function ComponentContainer(props) {
const CANCELLED = useRef({});
const last = useRef(
onlyLastRequestedPromise('search', () => CANCELLED)
);
const [searchQuery,setSearchQuery] = useState({});
const mounted = useIsMounted();
const onSearch = useCallback(
last(apiCall(searchQuery)).then(
items =>
items !== CANCELLED &&
mounted.current &&
//do something with items
)
);
}
Finally figured out how to utilize closure to mimic "just ignore that" approach from hooks' world:
class MyComponent extends React.Component {
const ignorePrevRequest = () => {}; // empty function by default
loadSomeData() {
this.ignorePrevRequest();
let cancelled = false;
this.ignorePrevRequest = () => { cancelled = true; }; // closure comes to play
doSomeCall().then(data => !cancelled && this.setState({ data }))
}
}

Resources