New component not rendering correctly - reactjs

const [data, setData] = useState([]);
const addOne = (newObj) => {
setData((prev) => {
return [newObj, ...prev];
});
};
The above is called and adds a new object to my array when I have finished making calls to my backend.
I get the data from there like so:-
const [posts, refreshPosts, loadingPosts, reachedEnd, resetPosts, addOne] =
usePaginate(
`post/wall/${id}/${toSkip}`,
{
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
},
setToSkip
);
Then pass the above into the feed component:-
const posts = props.posts.map((item) => {
return <Post data={item} />});
Where I create my post objects from the data, then inside post I create a new post and try to add the returned value to my array through the addOne method that I posted above:-
const handleClick = async () => {
const url = "post";
const options = {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
method: "POST",
body: JSON.stringify({ content: value, location: id }),
};
const post = await fetchData(url, options);
reset();
props.addOne(post)};
But it doesn't seem to trigger a re-render. Inside the post component I make calls that depend on the ID of the new object that was added, as an example:-
const [
commentData,
refreshComments,
loadingComments,
reachedEnd,
resetComments,
addOne,
] = usePaginate(
`post/comments/${props.data._id}/${toSkip}`,
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
method: "GET",
},
setToSkip
);
I would expect with a new object, an entirely new post to be created, but it doesn't seem to do that. What seems to happen is that the first element on the page re-renders partially
<div className="postContent">
{profData && props.data.content} </div>
So the text content of the top element on the page changes to reflect the correct text content of the post I'm trying to add, but then if I add a new comment to the new post, then add another post on top of that, only the props.data.content part changes, and the "shell" of the post remains the same (i.e. the entire post doesn't shift down, so the comments remain on the top post of the page, even though they should be on the second post at that point because the top post is now displaying the content from the newest data item).
resetComments();
refreshComments();
refreshLikes();
I can manually fire reach dataloader individually and then everything seems to work as expected, but it feels like I shouldn't have to do it because they should detect that a new item has been added to my array and just create an entirely new post object from it? It doesn't seem to be any issue with my backend because once I refresh the page everything is as should be. I can add any more information required, just didn't want to bloat the post so tried to pick the functions that seemed relevant to the problem, but I know something else could cause a side effect. It has been bugging me for hours.
The git link is here if anyone could have a look a bit more closely: -
https://github.com/Legandjl/odinbook_client
-edit
So far thanks to the comment I have changed return [newObj, ...prev] to return [...prev, newObj] and it seems to work okay when the new post is being rendered last, but then if I try to prepend or unshift the new object, like const newArr = [...prev], newArr.unshift(newObj) I either get a blackscreen or the same issue.

solved with the help of this post:-
problem when adding new item to start of array in react
const posts = props.posts.map((item, i) => {
return <Post key={item._id} data={item} />;
});
posts needed a unique key, tried with index and it didn't work but then used the _id and now all is as expected. Thanks all who tried to help me.

Related

React: Can't figure out how to use fetched API response

I'm working with a nice chatbot program for React someone wrote, and the thing is, you can actually bind the bot's responses to function calls like this:
render() {
const { classes } = this.props;
return (
<ChatBot
steps={[
{
...
{
id: '3',
message: ({ previousValue, steps }) => {
this.askAnswer(previousValue)
},
end: true,
},
]}
/>
);
Where message is the answer of the bot that it calculates based on the previousValue and askAnswer is a custom function you'd write. I'm using an API that inputs the previousValue to a GPT model, and I want to print the response of this API.
However, I just can't wrap my head around how I could pass the response of the API to the message. I'm trying this:
constructor(props) {
super(props);
this.state = { response: " " };
}
...
askAnswer(question) {
var jsonData = { "lastConversations": [question] }
fetch('http://my_ip:80/query', {
method: 'POST',
mode: 'cors',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(jsonData)
}).then(response => response.json())
.then(data => { this.setState({ response: data["botResponse"] }) });
return (this.state.response)
}
I've been struggling with this for the past 2-3 hours now.
I've tried a couple of combinations, and nothing seems to work:
If I do as seen above, it seems like this.state.response just won't get
updated (logging data["botResponse"] shows there is a correct
reply, so the API part works and I get the correct response).
If I try async-await for askAnswer and the fetch call, then I can
only return a Promise, which is then incompatible as input for the
ChatBot message.
If I do not await for the fetch to complete, then
this.state.response just stays the default " ".
If I return the correct data["botResponse"] from within the second .then after the fetch, nothing happens.
How am I supposed to get the API result JSON's data["botResponse"] text field out of the fetch scope so that I can pass it to message as a text? Or, how can I get a non-Promise string return after an await (AFAIK this is not possible)?
Thank you!
In your last 2 line
.then(data => { this.setState({ response: data["botResponse"] }) });
return (this.state.response)
You are updating the state and trying to read and return the value in the same function which I think will never work due to async behaviour of state. (this is why your this.state.response in the return statement is the previous value, not the updated state value).
I'm not sure but you can write a callback on this.setState and return from there, the callback will read the updated state value and you can return the new state.

How to create a class service provider that consumes an external context in React?

I'm trying to create a service class that provides the whole application with common access to an e xternal Rest api.
The first issue I face is that this external api needs to be consumed as a Context, so my first approach was to create a custom context to consume and configure all the rest calls. See code:
export const ApiContext = createContext();
const ApiProvider = ({ children }) => {
const api = externalApiContext();
const get = (url, params) =>
api.get(urlWithParams(`${baseUrl}${url}${admkey}`, params));
const post = (url, body, params = {}, headers = {}) =>
api.post(urlWithParams(`${baseUrl}${url}${admkey}`, params), {
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const put = (url, body, params = {}, headers = {}) =>
api.put(urlWithParams(`${baseUrl}${url}${admkey}`, params), {
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const remove = (url, params = {}, headers = {}) =>
api.remove(urlWithParams(`${baseUrl}${url}${admkey}`, params), {
headers: { ...headers, 'Content-Type': 'application/json' },
});
const fetch = (url) => api.get(`${url}`);
return (
<ApiContext.Provider value={{ get, post, put, remove, fetch }}>
{children}
</ApiContext.Provider>
);
};
This works fine. I can configure and make the calls all around the application.
The problem is: Whenever I make any of these calls, all of them are re-rendered as my context seems to have changed.
So I have 2 doubts:
Is this code correct to provide a general place to provide with a Context that provides with an external Rest api context? I am missing something that makes the context re-render?
By the other hand, I was trying to create a service class like posted here: https://newbedev.com/having-services-in-react-application . But then, inside a service class, a Context cannot be used... So...
Is there any way to create a common service Class that is able to consume an external Context api?
Thanks in advance!
As a start, you need to realize why your components re-render:
ApiProvider re-renders because, either it's parent component re-renders or because children changed. children is the most obvious candidate here.
The problem then is that every time ApiProvider renders, you provide a completely new context value:
<ApiContext.Provider value={{ get, post, put, remove, fetch }}>
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
\ new object everytime!
Which forces every consumer of that context value to re-render as well. The simplest solution would be to wrap everything with useCallback and the context value itself with useMemo.
Something like:
export const ApiContext = createContext();
const ApiProvider = ({ children }) => {
const api = externalApiContext();
const get = useCallback(
(url, params) => api.get(urlWithParams(`${baseUrl}${url}${admkey}`, params)),
[api]
);
// ...repeat for a others...
return (
<ApiContext.Provider value={useMemo(
() => ({ get, post, put, remove, fetch }),
[get, post, put, remove, fetch]
)} children={children}/>
);
};

react submit form then redirect issue

I have a form that must serve 2 tasks after submission:
send the form data to the server
redirect to another page
I'm having difficulties making both things happen;
The first one is easily accomplished using <Form action='/blabla'>, but then I get a blank page with the returned information from the server side as text.
The second one is also easily accomplished using <Form onSubmit={handleSubmit}> with the function:
const handleSubmit = (e) => {
e.preventDefault()
fetch('/blabla', {method: 'POST'})
.then(res => res.json())
.then(data => {
history.push('/nextPage')
})
.catch(error => {
alert(error)
})
}
And it works fine, except no data is sent from the form to the server :(
So, can someone explain me please how to get both tasks above done?
Thanks in advance :)
Would be more clear if you can post the as well.
Anyway, only from the snippet I say your fetch doesnot have body field in configuration, like:
fetch('/blabla', {
method: 'POST',
headers: {
'Content-Type': 'application/json' // or 'application/x-www-form-urlencoded'
},
body: JSON.stringify(data), // adjust this according to Content-Type header
})
that might be the reason why there was no data sent to server.

In React, how do I copy a state property by value?

I'm building a React 16.13 application. I want to copy a property from my state in order to manipulate it and leave the underlying state unchanged. I thought this was the way to do it ...
async handleFormSubmit(e) {
e.preventDefault();
const NC = [...this.state.newCoop]
delete NC.addresses[0].country;
try {
const response = await fetch(FormContainer.REACT_APP_PROXY + "/coops/", {
method: "POST",
body: JSON.stringify(this.state.newCoop),
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
});
However this line
const NC = [...this.state.newCoop]
gives the error
Unhandled Rejection (TypeError): this.state.newCoop is not iterable
What's the right way to copy a state variable by value?
I think it's just because you're spreading an object into an array; just do:
const NC = {...this.state.newCoop}
Edit:
Regarding the deep copy question, you can take a look at this answer:
https://stackoverflow.com/a/38417085
By the way, this has nothing to do with react :)

Component won't change with React router dom

I have an "Header" component and two more components that I want to show on it each at a different condition.
renderUserHeader = () =>{
if(sessionStorage.getItem('user-auth')){
var tokenToSend = {token: sessionStorage.getItem('user-auth')}
var regJSONED = JSON.stringify(tokenToSend)
fetch('http://localhost:4000/users/token', {
method: 'POST',
headers:{
"Content-Type": "application/json"
},
body: regJSONED,
}).then(response => {
if (!response.ok) {
throw new Error("HTTP error " + response.status);
}
return response.text();
})
.then(data => {
let JsonedUserName = JSON.parse(data)
this.setStateUserName(JsonedUserName.name)
})
return <UserHeader name ={this.state.userName}/>
} else if(!sessionStorage.getItem('user-auth')){
return <Link to="/login"> <LoginLink /> </Link>
}
}
As you can see component "UserHeader" will show only if 'user-auth' is exist in session storage, and component "LoginLink" will be showed if 'user-auth' doesn't exist.
Every time I delete 'user-auth' from the session-storage "LoginLink" is immediately being showed instead of "UserHeader", but every time "user-auth" is being created at the storage-session I must refresh the page so "UserHeader" can be showed up instead of "LoginLink".
How do I make "UserHeader" being shown immediately when "user-auth" is created?
Did I do something wrong in my code?
You can do setInterval to check if session storage data changed or not, better was doing this is using useEffect from React Hooks https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects

Resources