Fetch not working after switch from class to functional component - reactjs

I'm converting some class components to functional.
Console.log(data) returns the expected output but then once i try to set it using useState and check the value, it returns an empty array.
On the class component its working by using the state.
Functional Component ( Not Working )
const [submitting, setSubmitting] = useState(false)
const [watingForResult, setWaitingForResult] = useState(false)
const [submission, setSubmission] = useState([]);
const [scoringResults, setScoringResults] = useState([]);
+
function submitSubmission() {
setSubmitting(true);
setResult([]);
setError('');
let data = {
code: btoa(code),
language: { name: language.name },
users: { username: parseLocalJwt().username },
problem: { name: textToLowerCaseNoSpaces(problem.name) }
}
fetch(URL + '/submission', {
method: 'POST',
body: JSON.stringify(data),
headers: new Headers({
...getAuthorization(),
'Content-Type': 'application/json'
})
}).then(res => res.json()).then(data => {
setSubmitting(true);
setWaitingForResult(true);
setSubmission(data);
console.log('submitSubmission' + JSON.stringify(data));
window.secondsWaiting = new Date().getTime();
window.resultsListener = setInterval(fetchForResults(data.id), 1000);
});
}
...
function fetchForResults() {
console.log('data on fetchForResults' + submission.id)
}
...
<button
type="button"
onClick={submitSubmission}>
Submit!
</button>
console.log screenshot
Class component ( Working )
super(props);
this.state = {
problem: [],
sentSubmission: {
submitting: false,
waitingForResults: false,
submission: [],
scoringResults: []
},
results: {
loaded: false,
result: [],
error: ''
},
language: { mode: 'java', name: 'Java' },
code: ``
}
submitSubmission() {
this.setState({ sentSubmission: { submitting: true }, results: { result: [], error: '' } })
let data = {
code: btoa(this.state.code),
language: { name: this.state.language.name },
users: { username: parseLocalJwt().username },
problem: { name: textToLowerCaseNoSpaces(this.state.problem.name) }
}
fetch(URL + '/submission', {
method: 'POST',
body: JSON.stringify(data),
headers: new Headers({
...getAuthorization(),
'Content-Type': 'application/json'
})
}).then(res => res.json()).then(data => {
this.setState({ sentSubmission: { submitting: true, waitingForResults: true, submission: data } })
window.secondsWaiting = new Date().getTime();
window.resultsListener = setInterval(this.fetchForResults, 1000);
});
}

Some portion of the code is missing but, since you switched from a class component's state to a functional component state, I guess the issue is related to how you are using your state.
In functional components, when you set your state, you are changing the whole object in your state. So, when in your code you do setSentSubmission({ submitting: true });, your state becomes:
previousState = {
submitting: false,
waitingForResults: false,
submission: [],
scoringResults: []
}
nextState = {
submitting: true
//you lost watingForResults, submissions and scoring results
}
When using useState it is always suggested to decompose your object in different states:
const [submitting, setSubmitting] = useState(false)
const [watingForResult, setWaitingForResult] = useState(false)
//and so on...
//And then update them singularly:
//This...
setSentSubmission({ submitting: true });
//... become this
setSubmitting(true);
Finally, side note on lists. If you need to add a single element to a list in your state you can do:
setListState(currentList => [...currentList, newElement])
If this approach doesn't fit your use case and you need a more complex state management system I suggest you to look at useReducer (link).
Update
To print the content of your fetch you have either 2 choices.
Print directly the content retrieved in your fetch function
fetch('http://myendpoint.com/mydata.json')
.then(response => response.json())
.then(data => console.log(data));
If you need to do some processing to your data and you want to print what is the content of your updated state you can just use a useEffect hook. For instance, if you want to print the content of your scoringResult state you just write:
useEffect(() => {
console.log(scoringResult)
},[scoringResult])
This hook will be triggered every time scoringResult is updated (plus once when the component was mounted in the beginning)

Problem with your code is this:
setSentSubmission(...)
console.log(data);
console.log(sentSubmission.submission)
You can not set state and then to expect immediately to access that new state, new state can be accessed after component rerenders(in next iteration), all this is because you are reading sentSubmission.submission from closure which will be recreated only when component rerenders. So everything works fine with you code, you are just doing logging in a wrong place, move that log outside of the submitSubmission and you will see that state is updated successfully and that log will be printed after component rerenders(state updates).

When function submit() call, you set new value for sentSubmission.
Try this instead:
setSentSubmission((prevState) => (…prevState, { submitting: true }));

Related

React - Trying to update Fetch call in Parent Component based off of User Input in Child Component

So I am still very much a beginner when it comes to React. I am trying to build an application where the user inputs their location, which would then dynamically update the URL within my fetch call. Based off the results of the fist fetch call, I would then dynamically update a second fetch call, to a different API, providing me with the information needed.
As it stands right now, both Fetch calls are properly working, when provided the right information. The problem I am currently running into is, I don't believe my parent component is re-rendering with the update information. I am trying to console log one of my states but it keeps coming back as blank. The weird part is, the other state that is being created within the child component, is coming back with the right information. Any help would be greatly appreciated!
APP.JS
import Home from './Home/Home';
import Userinput from './UserInput/Userinput';
const url =
'https://api.openuv.io/api/v1/uv?lat=-33.34&lng=115.342&dt=2018-01-24T10:50:52.283Z';
const url1 = `http://www.mapquestapi.com/geocoding/v1/address?key=${process.env.REACT_APP_MAP_API_KEY}`;
class App extends Component {
constructor(props) {
super(props);
this.state = {
uvIndex: '',
lat: '',
long: '',
inputValue: '',
};
this.handleInputValue = this.handleInputValue.bind(this);
}
handleInputValue(val) {
this.setState({ inputValue: val });
}
componentDidMount() {
let newVal = this.state.inputValue;
fetch(`${url1}&location=${newVal}`, {
method: 'GET',
headers: {
'content-type': 'application/json',
},
})
.then((res) => res.json())
.then((res) => {
this.setState({
lat: res.results[0].locations[0].latLng.lat,
long: res.results[0].locations[0].latLng.lng,
});
// this.setState({ uvIndex: res });
})
.catch((err) => {
console.error(err);
});
fetch(url, {
method: 'GET',
headers: {
'content-type': 'application/json',
'x-access-token': `${process.env.REACT_APP_UV_API_KEY}`,
},
})
.then((res) => res.json())
.then((res) => {
console.log(res);
this.setState({ uvIndex: res });
})
.catch((err) => {
console.error(err);
});
}
render() {
console.log(this.state.lat); #this state comes back as blank
console.log(`${url1}&location=${this.state.inputValue}`); # this state comes back with the update userinput
return (
<div>
<header>
<Home />
</header>
<div>
<Userinput handleInput={this.handleInputValue} />
</div>
</div>
);
}
}
export default App;
Userinput.js
class Userinput extends Component {
constructor(props) {
super(props);
this.state = {
inputVal: '',
};
this.onInputChange = this.onInputChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
// handle input change event
onInputChange(e) {
this.setState({ inputVal: e.target.value });
}
// handle button click event and pass data in parent
handleSubmit() {
this.props.handleInput(this.state.inputVal);
}
render() {
return (
<div>
<input value={this.state.inputVal} onChange={this.onInputChange} />
<input type='button' value='Submit' onClick={this.handleSubmit} />
</div>
);
}
}
export default Userinput;
Consider this.setState in the App Class. The first fetch() is writing your state like so:
this.setState({
lat: res.results[0].locations[0].latLng.lat,
long: res.results[0].locations[0].latLng.lng,
});
The second fetch() call sets a new state object, which removes the result from the first fetch() call, or whatever fetch() resolves faster:
.then((res) => {
console.log(res);
this.setState({ uvIndex: res });
})
You can fix this issue with object spread operator:
this.setState({...this.state, uvIndex: res });
This will keep a copy of your state, and only overwrite uvIndex (if it was set before)
Also consider the handleInputVal Method in App.js. I think same problem here. the state is overwritten with a new Object (setState doesn't update the state, it creates a new State), which means that lat and long are being set to undefined 🤔
handleInputValue(val) {
//this.setState({ inputValue: val }); sets lat and long to undefined
this.setState({...this.state, inputValue: val }); //keep lat and long values, and update inputValue with val
}

Why is my cached state undefined on mutate in SWR?

I have a component that gets the data for a subreddit with posts and every post has a vote:
const subUrl = `/api/subreddit/findSubreddit?name=${sub}`;
const { data: fullSub, error } = useSWR(subUrl, fetchData, {
initialData: props.fullSub, //comes from next.js getServerSideProps
});
This works perfectly fine and it renders everything on the screen.
But on my subredditPost component (I render a subredditPost for each post) I have this onClick function when I click the upvote button:
const upvotePost = async (postid, fullSub, subUrl) => {
console.log("the sub variable is: ", subUrl); //same url as in the main component
mutate(
subUrl,
async (data) => {
// this is undefined when I hit on the upvote button
// Expected: The current state of swr with the key of subUrl
console.log("cached value: ", data);
return {
...data,
posts: data.posts.map((post) => {
if (post.id === postid) {
return {
...post,
votes: [...post.votes, { voteType: "UPVOTE" }],
};
} else {
return post;
}
}),
};
},
false
);
const res = await fetch("/api/post/upvote", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ postId: postid }),
});
mutate(subUrl);
};
It always is undefined in the console.log on the code above. So I get an error and nothing renders on the page anymore. What am I doing wrong? I read it can't get the cache if I use a different key, but I'm using the same key. I pass the subUrl from my main component (where I fetch the subreddit) to the post component (where I click the upvote button). So it is exactly the same.
Why do I get undefined?
I was able to solve this issue by providing a default value to mutate like this:
mutate(
sub,
async (data = fullSub) => {
console.log("cached value: ", data);
return {
...data,
posts: data.posts.map((post) => {
if (post.id === postid) {
return {
...post,
votes: [...post.votes, { voteType: "UPVOTE" }],
};
} else {
return post;
}
}),
};
},
false
);
So give the async function a default value if it's undefined: async (data = fullSub)
That fixes it but it's still not clear why it's undefined? Maybe somebody can answer this. I will leave this open for now, after a while if nobody has a better answer I will mark mine.

Why is a Component method using fetch and settingState in response not causing a componentDidMount()?

I want to generate a menu dynamically depending on user connected state and user role. I have a json file from which feeds the React app with all menu choices. The problem is that it does offer the "login" and "contact" options, which don't require any specific role or for a user to be logged in, but when I log in with the App's login() method, in which I use the fetch API and set the new state in the response, it doesn't refresh the menu choices (which is done in componentDidMount() method. It keeps serving me the login and contact options. I want to switch the login for logout when a user is connected.
I tried a bunch of stuff, but putting logs and debuggers in my code, I noticed the component doesn't re-render after the setState that's called in the login() fetch operation, but the state is indeed getting changed. I'm curious as to why the setState is not firing componentDidMount()?
menu.json
[
{
"txt": "login",
"idRole": null
},
{
"txt": "logout",
"idRole": null
},
{
"txt": "register",
"idRole": [
1
]
},
{
"txt": "profile",
"idRole": [
1,
2,
3
]
},
{
"txt": "contact",
"idRole": null
}
]
App.js
import React, { Component } from 'react'
import Header from 'container/Header.js'
import Footer from './container/Footer'
import Login from './container/Login'
import menu from '../json-form-file/menu.json'
export default class App extends Component {
constructor (props) {
super(props)
this.state = {
isMenuOpen: false,
isLoggedIn: false,
menu: null,
page: null,
user: null
}
this.toggleMenu = this.toggleMenu.bind(this)
this.selectPage = this.selectPage.bind(this)
this.login = this.login.bind(this)
this.logout = this.logout.bind(this)
}
toggleMenu () {
this.setState({ isMenuOpen: !this.state.isMenuOpen })
}
selectPage (event) {
this.setState({ isMenuOpen: !this.state.isMenuOpen, page: event.target.textContent })
const toggler = document.getElementsByClassName('toggler')[0]
toggler.checked = !toggler.checked
}
login (event) {
event.preventDefault()
const requestBody = createLoginRequestBody(Array.from(event.target.parentElement.children))
clearLoginFields(Array.from(event.target.parentElement.children))
if (requestBody.username !== undefined && requestBody.pwd !== undefined) {
fetch('www.someLoginUrl.login', {
method: 'post',
body: JSON.stringify(requestBody),
headers: { 'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(response => this.setState({ user: response, isLoggedIn: true, page: null }))
}
}
logout (event) {
event.preventDefault()
const toggler = document.getElementsByClassName('toggler')[0]
toggler.checked = !toggler.checked
this.setState({ user: null, isLoggedIn: false, page: null, isMenuOpen: !this.state.isMenuOpen })
}
componentDidMount () {
console.log('im mounting')
const newMenu = this.refreshMenuSelection(menu)
this.setState({ menu: newMenu })
}
refreshMenuSelection (list) {
const newMenu = []
list.map((item) => {
if (item.txt === 'login' && this.state.isLoggedIn === false) newMenu.push(item)
if (item.txt === 'logout' && this.state.isLoggedIn === true) newMenu.push(item)
if (item.idRole === null && item.txt !== 'login' && item.txt !== 'logout') newMenu.push(item)
if (this.state.user !== null && item.idRole.includes(this.state.user.id_role)) newMenu.push(item)
})
return newMenu
}
render () {
return (
<div>
<Header
menu={this.state.menu}
toggleMenu={this.toggleMenu}
selectPage={this.selectPage}
logout={this.logout}
color={this.state.isMenuOpen ? secondaryColor : primaryColor} />
{this.state.page === 'login' ? <Login login={this.login} /> : null}
<Footer color={this.state.isMenuOpen ? secondaryColor : primaryColor} />
</div>
)
}
}
const createLoginRequestBody = (inputs) => {
const requestObject = {}
inputs.map((input) => {
if (input.id === 'username') Object.assign(requestObject, { username: input.value })
if (input.id === 'pwd') Object.assign(requestObject, { pwd: input.value })
})
return requestObject
}
When a user is not logged in, he could see only login and contact. When logged in, he could see logout instead of login, contact and all other choices relevant to his role.
Nothing causes a componentDidMount to run again, it's a lifecycle hook which runs only one time through the component's lifecycle. Everything that goes inside componentDidMount will only run once (after the first render), so if you need to react to a change to perform imperative code, take a look at componentDidUpdate. You should also take a look in the documentation
As said, componentDidMount only runs once when the component is first mounted. If you use setState() after the first mount, the function will not respond to it. If you want to do that, maybe you ought to use componentDidUpdate which does react to this type of change. And also, there's another thing wrong with your code. If you use setState() and use componentDidUpdate then it will change the state again and calling the function again until the program crashes. So if you don't want to cause that, maybe also remove that or move it to a new componentDidMount function.
Thanks to everyone who guided me to the componentDidUpdate() method. This modified bit of code helped me achieve what I wanted. In the future I'll clean up the code to remove the justLoggedIn, as that might not be necessary, but without that it was giving my the setState depth error.
login (event) {
event.preventDefault()
const requestBody = createLoginRequestBody(Array.from(event.target.parentElement.children))
clearLoginFields(Array.from(event.target.parentElement.children))
if (requestBody.username !== undefined && requestBody.pwd !== undefined) {
fetch('http://127.0.0.1:8080/user/login', {
method: 'post',
body: JSON.stringify(requestBody),
headers: { 'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(response => this.setState({ user: response, justLoggedIn: true, isLoggedIn: true }))
}
}
logout (event) {
event.preventDefault()
const toggler = document.getElementsByClassName('toggler')[0]
toggler.checked = !toggler.checked
this.setState({ user: null, justLoggedOut: true, isLoggedIn: false, isMenuOpen: !this.state.isMenuOpen })
}
componentDidMount () {
if (this.state.user === null) this.setState({ menu: this.refreshMenuSelection(menu) })
}
componentDidUpdate () {
if (this.state.user !== null && this.state.justLoggedIn) this.setState({ menu: this.refreshMenuSelection(menu), justLoggedIn: false, page: null })
if (this.state.user === null && this.state.justLoggedOut) this.setState({ menu: this.refreshMenuSelection(menu), justLoggedOut: false, page: null })
}

componentDidUpdate keeps getting called

componentDidUpdate () {
this.showPosts();
}
showPosts = async () => {
var userID = await AsyncStorage.getItem('userID');
fetch(strings.baseUri+"getPostWithUserID", {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"user_id": userID
})
})
.then((response) => response.json())
.then((responseJson) => {
let jsonObj = JSON.parse(JSON.stringify(responseJson));
if (jsonObj.status=="true") {
this.setState({
data: responseJson.data,
imageSrc: responseJson.data.imgSrc,
});
}
else {
this.setState({show: false});
}
})
}
I'm calling showPosts function from componentDidUpdate to show my updated Flatlist. But componentDidUpdate keeps getting called. Should I use shouldComponentUpdate ?
========================== UPDATED CODE ============================
This is from Home Screen
async componentDidMount () {
this._isMounted = true;
await this.showPosts();
}
componentDidUpdate () {
this.showPosts();
}
showPosts = async () => {
try {
var userID = await AsyncStorage.getItem('userID');
fetch(strings.baseUri+"getPostWithUserID", {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"user_id": userID
})
})
.then((response) => response.json())
.then((responseJson) => {
let jsonObj = JSON.parse(JSON.stringify(responseJson));
if (jsonObj.status=="true") {
this.setState({
data: responseJson.data,
imageSrc: responseJson.data.imgSrc,
});
}
else {
if (this._isMounted) {
this.setState({show: false});
}
}
})
.catch((error) => {
console.error(error);
});
}
catch (err) {
console.warn(err);
}
}
componentWillUnmount () {
this._isMounted = false;
}
This is Image Descrpiption screen from where I'll navigate back to Home Screen
postData = async () => {
this.setState({loading: true});
var location = await AsyncStorage.getItem('location');
var path = await AsyncStorage.getItem('path');
var post_type = await AsyncStorage.getItem('post_type');
var userId = await AsyncStorage.getItem('userID');
var newPath = path.split("/");
var imageName = newPath[newPath.length-1];
const formData = new FormData();
var media = {
uri: path,
name: imageName,
type: 'image/jpg',
};
formData.append('image', media);
formData.append('user', userId);
formData.append('description',this.state.description);
formData.append('location',"usa");
formData.append('post_type',post_type);
formData.append('userprofile_picture',imageName);
fetch(strings.baseUri+"addPosts",{
method: 'POST',
headers: { 'Content-Type': 'multipart/form-data' },
body: formData,
})
.then((response) => response.json())
.then((responseJson) => {
let jsonObj = JSON.parse(JSON.stringify(responseJson));
if (jsonObj.status=="true") {
this.props.navigation.popToTop()
&& this.props.navigation.navigate('Home'); // This navigates me to the HomeScreen
}
else {
}
})
.catch((error) => {
console.error(error);
});
}
ComponentDidUpdate is an update LifeCycle Hook, this will get triggered when there is something is changed in the component State or Props.
Coming to your code:
You are calling a handler showPosts to setState, that will again trigger the update lifecycle.
This will lead to an infinite loop.
Solution
If you want to load the posts only in the first time, then move to Creational Life Cycle hook ( componentDidMount ).
componentDidMount() { // This just gets called once in creational lifecycle //
this.showPosts(); }
if you want this to always have the latest data, then there are two ways
Updating component is in the same component tree branch:, In this case , it's easy to achieve this you can pass the state from the updating component down to child component has props, your job is done OR if they are siblings then do a level up you can move the state one level up and have it coming in has props.
Updating component is in the different component tree branch: I recommend using REDUX, this is the main use of redux.
shouldComponentUpdate Yes definitely you can use this to verify the data and the load if needed, but be careful by using this your components update depends on the code in this.
Please check https://reactjs.org/docs/react-component.html#shouldcomponentupdate
You just need to call this in that way if you do in ComponentDidUpdate and update state in the method call by ComponentDidUpdate then a infinite loop start.
componentDidMount () {
this.showPosts();
}
================EDITED=======================
If you want to use only ComponentDidUpdate then you can use it like.
componentDidUpdate(prevProps, prevState) {
// only update if not match I don't know what's your data is so add a
// simple check like we use for strings.
if (prevState.data !== this.state.data) {
this.showPosts();
}
}
Just use prevState to match.
You can do this too
Common parent component
Create a new component say Posts.
import React, { Component } from 'react';
import HomeScreen from '../../HomeScreen';
import ImageDescription from '../../ImageDescription';
class Posts extends Component {
constructor(){
super();
this.state = {
dataEditted: false;
}
}
newDataHandler = () =>{
this.setState({dataEditted:true}); // this is flag to identify that there is change in data //
}
resetnewDataHandler = () =>{
this.setState({dataEditted:false}); // this handler is used to reset the falg back to initial //
}
render () {
const homeScreen = <HomeScreen editted={this.state.editted} resetHandler={this.resetnewDataHandler}/>;
const imageDescription = <ImageDescription newdataHandler={this.resetnewDataHandler}/>
return (
<div>
{homeScreen}
{imageDescription}
</div>
)
}
}
export default Posts;
This component is going to serve as a bridge to move data between.
Whenever there is fresh data in ImageDescription Component use the newDataHandler passed has props to update the common parent, then the dataEditted will be updated and passed has props to homeScreen Component, now in this componentDidUpdate of homeScreen check whether its true, then call this.showPosts() and also call resetnewDataHandler.
componentDidUpdate() is called when the state is changed (calling setState()) and if you do it inside the showPosts that is also inside the componentDidUpdate() you are creating an infinite state updating.

Fetching a data text from API response and presenting it in a chat with React

I have a chat UI in react in which the user should input text and receive some data from an API response service. The UI works fine and the user text is presented in the chat but the response from the API which received in JSON format doesn't presented in the chat at all (not even an error message from the API, which should appear in the 'message' field,which is the same field where the result should be presented).As far As I know I configured everything ok, I don't know if something is messing in the render method though.
Note: the preconfigured messages at the chat state in the beginning are just for testing purposes.
The Fetch operation in under the componentDidUpdate() method at the first block of code that I posted.
import React from "react";
import ReactDOM from "react-dom";
import "./App.css";
import Message from "./Message.js";
class Chatroom extends React.Component {
constructor(props) {
super(props);
this.state = {
chats: [
{
username: "clientUser",
content: <p>Hello World!</p>,
img:
"http://***.jpg"
},
{
username: "user2",
content: <p>Hi,my name is user2.What's up ??</p>
},
{
username: "user3",
content: <p>Hi,my name is user3.What's up ??</p>
},
{
username: "user4",
content: <p>Hi,my name is user4.What's up ??</p>
},
{
username: "userN",
content: <p>Hi,my name is userN.What's up ??</p>,
img: "http://***.jpg"
},
{
username: "user5",
content: <p>Hi,my name is user5.What's up ??</p>
},
{
username: "user6",
content: <p>Hi,my name is user6.What's up ??</p>
},
{
username: "user7",
content: <p>Hi,my name is user7.What's up ??</p>
}
]
};
this.submitMessage = this.submitMessage.bind(this);
}
componentDidMount() {
this.scrollToBot();
}
componentDidUpdate() {
this.scrollToBot();
fetch(
"https://****",
{
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify({
inputText: <p>{ReactDOM.findDOMNode(this.refs.msg).value}</p>
})
}
).then(response => response.json())
.then(parsedJSON =>
parsedJSON.results.map((
user
) => ({
username: "BotResponse",
content: `${user.message}',
img: "http://***.jpg"
}))
)
.then(chats =>
this.setState({
chats
})
)
.catch(error => console.log("parsing failed", error));
}
scrollToBot() {
ReactDOM.findDOMNode(this.refs.chats).scrollTop = ReactDOM.findDOMNode(
this.refs.chats
).scrollHeight;
}
The submit message method:
submitMessage(e) {
e.preventDefault();
this.setState(
{
chats: this.state.chats.concat([
{
username: "clientUser",
content: <p>{ReactDOM.findDOMNode(this.refs.msg).value}</p>,
img: "http://***.jpg"
}
])
},
() => {
ReactDOM.findDOMNode(this.refs.msg).value = "";
}
);
}
The render method:
render() {
const username = "clientUser";
const { chats } = this.state;
return (
<div className="chatroom">
<h3>
Title
</h3>
<ul className="chats" ref="chats">
{chats.map((
chat //Defines message component
) => (
<Message chat={chat} user={username} />
))}
</ul>
<form className="input" onSubmit={e => this.submitMessage(e)}>
<input type="text" ref="msg" />
<input type="submit" value="Submit" />
</form>
</div>
);
}
}
export default Chatroom;
The problem lies in the way you think. You are thinking like if you were writing plain HTML with JS enhancements. React is a fundamental concept shift. You need to forget a lot of what you knew from regular, vanilla JS, to learn React. In React, you don't get HTMLElement's to do operations but rather work with local/global state and component props to declare your UI. The resulting code is more performant, maintainable and glitch-free.
To get back to your actual issue, you shouldn't be using ref to get the value of your input. Instead, use onChange to set state of your message and retrieve it later when needed.
For example:
export class MessageComposer extends React.Component {
state = {
fields: {
message: ""
}
}
clearField = fieldName => {
this.setState({
fields: {
[fieldName]: ""
}
})
}
onInputChange = e => {
this.setState({
fields: {
[e.target.name]: e.target.value
}
})
}
sendMessage = () => {
const { fields: { message } } = this.state
// Here you send the message contained into `message`.
console.log(message)
// Then you clean the message input value.
this.clearField("message")
}
render () {
const { fields: { message } } = this.state
return (
<div className="MessageComposer__container">
<input
type="text"
name="message"
value={message}
onChange={this.onInputChange}
/>
<button onClick={this.sendMessage}>
Submit
</button>
</div>
)
}
}
EDIT: Just saw that your fetch code includes JSX. WTF?
Try by changing the following:
body: JSON.stringify({
inputText: <p>{ReactDOM.findDOMNode(this.refs.msg).value}</p>
})
with JSX-free version. It doesn't make any sense to do pass a React component as JSON.stringify isn't even able to serialize a function (ok you'll receive i.e 'Function' or '[Object object]' into inputText on your backend i guess).
Somthing like this:
body: JSON.stringify({
inputText: ReactDOM.findDOMNode(this.refs.msg).value
})
Also, if for any reason React doesn't find your ref (i.e. component returns null or any other reason), the inputText will either throw or return undefined.
If you refactor your code as I suggested above, you could do this:
body: JSON.stringify({
inputText: this.state.fields.message
})
There is a fair amount to discuss regarding your code. To keep this answer simple and focused, I am just going to address your fetch api call and why you may not be seeing anything on the front end.
First, I suggest looking into Life Cycle Methods in the React Documentation. You can find that here. If that link is broken, just go to React's documentation page and search "LifeCycle Methods".
The reason I bring that up is because you are posting your data every time the component updates not when the user submits their chat message. So, the moment the user does submit you call your onSubmit function which calls setState. Setting the state will update the post. So anywhere in your code, when you make changes to your state or if there are changes to your component's props the DOM will update and componentDidUpdate will be triggered and you will post again.
Place your post api call in your onSubmit function. Update your form from this:
submitMessage(e) {
e.preventDefault();
this.setState(
{
chats: this.state.chats.concat([
{
username: "clientUser",
content: <p>{ReactDOM.findDOMNode(this.refs.msg).value}</p>,
img: "http://***.jpg"
}
])
},
() => {
ReactDOM.findDOMNode(this.refs.msg).value = "";
}
);
}
to this updated onSubmit function:
submitMessage(e) {
e.preventDefault();
fetch(
"https://****",
{
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify({
inputText: <p>{ReactDOM.findDOMNode(this.refs.msg).value}</p>
})
}
).then(response => response.json())
.then(parsedJSON => {
parsedJSON.results.map((
user
) => ({
username: "BotResponse",
content: `${user.message}',
img: "http://***.jpg"
}))
}
)
.then(chats =>
this.setState({
chats
})
)
.catch(error => console.log("parsing failed", error));
//for the sake of readability and debugging, you might want to store
that obj in a local variable here and put that variable in place of ...[]
this.setState(
{
chats: [
...this.state.chats,
{
username: "clientUser",
content: <p>{ReactDOM.findDOMNode(this.refs.msg).value}</p>,
img: "http://***.jpg"
}
]
},
() => {
ReactDOM.findDOMNode(this.refs.msg).value = "";
}
);
}
Back-end
You want to confirm that this is actually saving to your db (may seem obvious but double check that it is saving the way you want and that you are returning the correct info. in the correct format on your back-end. This could be where the problem is. Log your response to your back-end console and make sure it is returning everything correctly.
Front-End
Look at how you are setting your state in your fetch call. I'm not really sure how you are sending your data but here is an example of a post fetch call using axios.
axios.post("http://****", {
data: this.state.data,
more: this.state.more,
etc: { example: "of", object: this.state.obj }
})
.then(response => {
if (response.status === 200) {
//do something with response.data
//this.setState({ data: response.data})
}
});
There really isn't anything more I can suggest. I'm not really sure what you are trying to achieve with the source you have provided.
Note
There are a few things that you are doing throughout your code that I'm not sure you need to do. But, I did not address that in this answer because I'm not entirely sure what your project is and your desired results. But this should help with the issue of nothing rendering. If it doesn't, look into how you are setting your state in the fetch api call.
Hope this helps.
EDIT
I don't believe you need to have three .then in your api call. Once you have parsed your json you can set your state there.
EDIT 2
The above source was not giving the desired result. The fetch api call was not returning data to the second then call because the data from the first then call. Here is the updated submitMessage function.
submitMessage(e) {
e.preventDefault();
fetch(
"https://****",
{
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify({
inputText: <p>{ReactDOM.findDOMNode(this.refs.msg).value}</p>
})
}
).then(response => {
return response.json() //here was the problem, nothing was being returned
})
.then(parsedJSON => {
parsedJSON.results.map(user => ({
username: "BotResponse",
content: user.message, //this shouldn't need to be a string literal
img: "http://***.jpg"
})
)
}
)
.then(chats =>
this.setState({
chats
})
)
.catch(error => console.log("parsing failed", error));
}

Resources