React Chatbox, how to get the string displayed? - reactjs

I am a newbie, and am trying to build a simple restaurant recommendation web app using AWS and React. So, I am using this chat window(https://www.npmjs.com/package/react-chat-window). Basically, when the user types something, the chatbot gets triggered and asks questions like "what kind of food do you want?" So far, I am able to pass the user's input and get the response back from the AWS. I can log the response to the console and verify it. But I have trouble getting the response displayed in the chatbox.
Here is the snippet of the code
class chatBox extends Component {
constructor() {
super();
this.state = {
messageList: chatHistory,
newMessagesCount: 0,
isOpen: false
};
}
// message is the user's input
_onMessageWasSent(message) {
var body = {
messages: message.data['text']
}
// every time the user types something, this function passes the user's input to AWS
apigClient.invokeApi(pathParams, pathTemplate, method, additionalParams, body)
.then(function (result) { // result contains the response to the user's input
var text = result.data.body
console.log(text) // logs the response to the user's input
console.log(text.length)
}).catch(function (result) {
});
this.setState({ //this displays what the user types
messageList: [...this.state.messageList, message]
})
}
// This is a function that displays the input of the other side
// I can manually test it and see that whatever I pass to this function gets displayed as
// the other person's speech, not the user.
_sendMessage(text) {
console.log("sendMessage")
if (text.length > 0) {
this.setState({
messageList: [...this.state.messageList, {
author: 'them',
type: 'text',
data: { text }
}],
newMessagesCount: this.state.newMessagesCount + 1
})
}
}
As can be seen, I am logging the response to the console. Now, I want to get the response displayed so I tried inside the constructor
this._onMessageWasSent = this._sendMessage.bind(this)
and calling the function inside _onMessageSent
apigClient.invokeApi(pathParams, pathTemplate, method, additionalParams, body)
.then(function (result) { // result contains the response to the user's input
var text = result.data.body
console.log(text) // logs the response to the user's input
console.log(text.length)
this._sendMessage(text) // Calling the function
}).catch(function (result) {
});
this.setState({ //this displays what the user types
messageList: [...this.state.messageList, message]
})
}
I can see that the _sendMessage function gets triggered, because I have a console.log. But now the chatbox displays neither the user and the chatbot. If I don't bind this._onMessageWasSent = this._sendMessage.bind(this), at least I get the user displayed.
What could be the problem??
This is my render()
render() {
return (<div>
<Launcher
agentProfile={{
teamName: 'Foodophile',
imageUrl: 'https://a.slack-edge.com/66f9/img/avatars-teams/ava_0001-34.png'
}}
onMessageWasSent={this._onMessageWasSent.bind(this)}
messageList={this.state.messageList}
onFilesSelected={this._onFilesSelected.bind(this)}
newMessagesCount={this.state.newMessagesCount}
handleClick={this._handleClick.bind(this)}
isOpen={this.state.isOpen}
showEmoji
/>
</div>)
}
UPDATE
class chatBox extends Component {
constructor(props) {
super(props);
this.state = {
messageList: chatHistory,
newMessagesCount: 0,
isOpen: false
};
this._onMessageWasSent = this._onMessageWasSent.bind(this);
this._onFilesSelected = this._onFilesSelected.bind(this);
this._handleClick = this._handleClick.bind(this);
this._sendMessage = this._sendMessage.bind(this);
}
_onMessageWasSent(message) {
var body = {
messages: message.data['text']
}
apigClient.invokeApi(pathParams, pathTemplate, method, additionalParams, body)
.then(function (result) {
var text = result.data.body
console.log(text)
console.log(text.length)
this._sendMessage(text)
}).catch(function (result) {
});
this.setState({
messageList: [...this.state.messageList, message]
})
}
_sendMessage(text) {
console.log("sendMessage")
if (text.length > 0) {
this.setState({
messageList: [...this.state.messageList, {
author: 'them',
type: 'text',
data: { text }
}],
newMessagesCount: this.state.newMessagesCount + 1
})
}
}
render() {
return (<div>
<Launcher
agentProfile={{
teamName: 'Foodophile',
imageUrl: 'https://a.slack-edge.com/66f9/img/avatars-teams/ava_0001-34.png'
}}
onMessageWasSent={this._onMessageWasSent}
messageList={this.state.messageList}
onFilesSelected={this._onFilesSelected}
newMessagesCount={this.state.newMessagesCount}
handleClick={this._handleClick}
isOpen={this.state.isOpen}
showEmoji
/>
</div>)
}

You have to bind your class methods in class components in order to call them with this. But you have to do this, e.g. in the constructor BUT not in your render function!
Check out this very nice explanation on why and how to bind your functions.
constructor( props ){
super( props );
this._onMessageWasSent = this._onMessageWasSent.bind(this);
this._onFilesSelected = this._onFilesSelected.bind(this);
this._handleClick = this._handleClick.bind(this);
this._sendMessage = this._sendMessage.bind(this);
}
In your render function, just pass the functions like follows:
render() {
return (<div>
<Launcher
agentProfile={{
teamName: 'Foodophile',
imageUrl: 'https://a.slack-edge.com/66f9/img/avatars-teams/ava_0001-34.png'
}}
onMessageWasSent={this._onMessageWasSent}
messageList={this.state.messageList}
onFilesSelected={this._onFilesSelected}
newMessagesCount={this.state.newMessagesCount}
handleClick={this._handleClick}
isOpen={this.state.isOpen}
showEmoji
/>
</div>)
}
Also, there is one more issue. This binding is a tricky thing in JavaScript and function vs ()=>{} arrow functions do treat this differently. In your case, just use an arrow function instead.
apigClient.invokeApi(pathParams, pathTemplate, method, additionalParams, body)
.then((result) => {
var text = result.data.body
console.log(text)
console.log(text.length)
this._sendMessage(text)
}).catch(function (result) {
});
This will make sure that this inside your then-callback function is still the this that you expect it to be. This is why, if you would refactor all your functions (_onMessageWasSent, _onMessageWasSent, _onFilesSelected, handleClick, _sendMessage ) to arrow functions, there is no need anymore to bind them to this in the constructor.
See this for example:
_onMessageWasSent = (message) => {
// your function body
}
You could already get rid of the line this._onMessageWasSent = this._onMessageWasSent.bind(this);.
Read more about this binding in functions at w3school.

Related

React testing error "Cannot read get property of undefined"

I am trying to test the function searchTrigger in my CardMain component.
export default class CardMain extends Component {
state = {
Pools : [],
loading: false,
}
componentDidMount(){
axios.get('/pools')
.then (res => {
//console.log(res.data.data);
this.setState({
Pools: res.data.data,
loading: true,
message: "Loading..."
},()=>{
if (res && isMounted){
this.setState({
loading: false
});
}
})
}
)
.catch(err=>{
console.log(err.message);
})
}
// the function is for search method
// upon search, this function is called and the state of the pools is changed
searchTrigger = (search) => {
Search = search.toLowerCase();
SearchList = this.state.Pools.filter((e)=> {
if (e.name.toLowerCase().includes(Search)){
this.setState({
loading: false
})
return e
}
})
if (SearchList.length === 0){
this.setState({
loading: true,
message: "No pools found"
})
}
}
render() {
return (
<div>
<Searchbar trigger={this.searchTrigger}/>
{ this.state.loading ?
<div className="d-flex justify-content-center">{this.state.message}</div>
:<div>
{Search === "" ? <Card1 pools={this.state.Pools}/> : <Card1 pools={SearchList}/> }
</div>
}
</div>
)
}
}
The function searchTrigger is passed to another class component called Searchbar which basically displays the search bar. Upon searching something, the function searchTrigger is called and the searched value is passed as an argument to this function.
So, I am trying to test this function and I am new to react and testing. I found some examples online and tried a simple testing whether the function is called or not. My CardMain.test.js code looks like this:
describe("callback function test", ()=> {
it("runs it", () => {
//const spy = jest.spyOn(CardMain.prototype,"searchTrigger");
const cardmain = shallow(<CardMain/>)
const spy = jest.spyOn(cardmain.instance(), "searchTrigger");
expect(spy).toHaveBeenCalled()
})
});
I get the TypeError: Cannot read property 'get' of undefined pointing to the axios.get("/pools") in the CardMain component inside componentDidMount. axios is being imported from another component api.js which creates the instance of axios using axios.create. I have no idea what the problem is. I am very new to react. I have absolutely no idea, how do I test these components? Could somebody help me?
Update:
So, i tried mocking axios call:
let Wrapper;
beforeEach(() => {
Wrapper = shallow( <CardMain/>);
});
describe("Card Main", ()=> {
it("returns data when called", done => {
let mock = new MockAdapter(axios);
const data = [{
name: "Test",
response: true
}];
mock.onGet('My_URL')
.reply(200,data);
const instance = Wrapper.instance();
instance.componentDidMount().then(response => {
expect(response).toEqual(data);
done();
});
});
});
It says "cannot read property .then of undefined"

React - Render happening before data is returned and not updating component

I can't get this to work correctly after several hours.
When creating a component that needs data from Firebase to display, the data is returning after all actions have taken place so my component isn't showing until pressing the button again which renders again and shows correctly.
Currently my function is finishing before setState, and setState is happening before the data returns.
I can get setState to happen when the data is returned by using the callback on setState but the component would have already rendered.
How do i get the component to render after the data has returned?
Or what would the correct approach be?
class CoffeeList extends Component {
constructor(props) {
super(props);
this.state = {
coffeeList: [],
}
}
componentDidMount() {
this.GetCoffeeList()
}
GetCoffeeList() {
var cups = []
coffeeCollection.get().then((querySnapshot) => {
querySnapshot.forEach(function (doc) {
cups.push({ name: doc.id})
});
console.log('Updating state')
console.log(cups)
})
this.setState({ coffeeList: cups })
console.log('End GetCoffeeList')
}
render() {
const coffeeCups = this.state.coffeeList;
console.log("Rendering component")
return (
<div className="coffee">
<p> This is the Coffee Component</p>
{coffeeCups.map((c) => {
return (
<CoffeeBox name={c.name} />
)
})}
</div >
)
}
}
Thanks
The problem is that you set the state before the promise is resolved. Change the code in the following way:
GetCoffeeList() {
coffeeCollection.get().then((querySnapshot) => {
const cups = []
querySnapshot.forEach(function (doc) {
cups.push({ name: doc.id})
});
console.log('Updating state')
console.log(cups)
this.setState({ coffeeList: cups })
console.log('End GetCoffeeList')
})
}

Why this setState call does not trigger rendering?

I have made a small treeview component, which gets a json file with the structure and should render the treeview.
The json is correct, the promise on the constructor is resolved correctly, but the render is not called after the setState inside the constructor.
I´ve tried to read similar questions, but no success.
export default class Tree extends Component<ITreeProps, ITreeState> {
public constructor(props: ITreeProps, state: ITreeState) {
super(props, state);
Tree.GetJsonStructure(props.Path).then(response => {
this.setState({
data : JSON.parse(response)
});
console.log(response);
});
}
public static GetJsonStructure(jsonPath: string): Promise<string>{
return new Promise((resolve, reject) => {
axios.get(jsonPath, { responseType: 'arraybuffer' }).then(response => {
if (response.status !== 200) {
// handle error
reject("FAIL!");
}
var buf = new Buffer(response.data);
//var buf = Buffer.concat(response.data);
var paki = pako.inflate(buf);
var decoder = new encoding.TextDecoder();
var stringo = decoder.decode(paki);
resolve(stringo);
});
});
}
public render(): React.ReactElement<ITreeProps> {
console.log("RENDERINGGGG");
console.log(this.state)
if(!this.state || !this.state.data || this.state.data.length == 0 ){
return <div></div>;
}
const data = this.state.data;
if (!data || data.length == 0)
console.log("No properties set for the application");
return (
<div className={styles.ToolboxLinkPanel}>
{
data.map(node => (
<TreeNode key={node.key} label={node.label} children={node.nodes} isOpen={true} ></TreeNode>
))
}
</div>
);
}
}
You need to call your api method inside componentDidMount and not in the constructor:
componentDidMount() {
Tree.GetJsonStructure(props.Path).then(response => {
this.setState({
data: JSON.parse(response)
});
console.log(response);
});
}
As to why it's the place to make api calls, the official documentation elaborates (emphasis added):
componentDidMount() is invoked immediately after a component is
mounted (inserted into the tree). Initialization that requires DOM
nodes should go here. If you need to load data from a remote endpoint,
this is a good place to instantiate the network request [...] It will trigger an extra rendering, but it will happen before the browser updates the screen..

Unable to pass params successfully to another .js file/screen

I'm trying to pass params from one screen to another screen using react-navigation, the problem I'm encountering is that when I console.log the param itself, the console returns 'undefined'. I can't seem to pinpoint what I'm doing wrong exactly. Any help or guidance would be much appreciated.
I tried the following, with no success:
-this.props.navigation.getParam('biometryStatus')
-this.props.navigation.state.params('biometryStatus')
This is my AuthenticationEnroll screen where my param is being initialised as the state of the component:
export default class AuthenticationEnroll extends Component {
constructor() {
super()
this.state = {
biometryType: null
};
}
async _clickHandler() {
if (TouchID.isSupported()){
console.log('TouchID is supported');
return TouchID.authenticate()
.then(success => {
AlertIOS.alert('Authenticated Successfuly');
this.setState({biometryType: true })
this.props.navigation.navigate('OnboardingLast', {
pin: this.props.pin,
biometryStatus: this.state.biometryType,
});
})
.catch(error => {
console.log(error)
AlertIOS.alert(error.message);
});
} else {
this.setState({biometryType: false });
console.log('TouchID is not supported');
// AlertIOS.alert('TouchID is not supported in this device');
}
}
_navigateOnboardingLast() {
this.props.navigation.navigate('OnboardingLast', {pin: this.props.pin})
}
render () {
return (
<View style={{flex: 1}}>
<Slide
icon='fingerprint'
headline='Secure authentication'
subhead='To make sure you are the one using this app we use authentication using your fingerprints.'
buttonIcon='arrow-right'
buttonText='ENROLL'
buttonAction={() => this._clickHandler()}
linkText={'Skip for now.'}
linkAction={() => this._navigateOnboardingLast()}
slideMaxCount={4}
slideCount={2}
subWidth={{width: 220}}
/>
</View>
)
}
}
And this is my OnboardingLast Screen where my param is being passed down and printed through console.log:
class OnboardingLast extends Component {
async _createTokenAndGo () {
let apiClient = await this._createToken(this.props.pin)
this.props.setClient(apiClient)
AsyncStorage.setItem('openInApp', 'true')
const { navigation } = this.props;
const biometryStatus = navigation.getParam('biometryStatus', this.props.biometryStatus);
console.log(biometryStatus);
resetRouteTo(this.props.navigation, 'Home')
}
/**
* Gets a new token from the server and saves it locally
*/
async _createToken (pin) {
const tempApi = new ApiClient()
let token = await tempApi.createToken(pin)
console.log('saving token: ' + token)
AsyncStorage.setItem('apiToken', token)
return new ApiClient(token, this.props.navigation)
}
render () {
return (
<View style={{flex: 1}}>
<Slide
icon='checkbox-marked-circle-outline'
headline={'You\'re all set up!'}
subhead='Feel free to start using MyUros.'
buttonIcon='arrow-right'
buttonText='BEGIN'
buttonAction={() => this._createTokenAndGo()}
slideMaxCount={4}
slideCount={3}
/>
</View>
)
}
}
Expected Result is that console.log(biometryStatus); returns 'true' or 'false', however it returns 'undefined'.
Since setState is asynchron, you send null (declared in your constructor) to your next page. By doing so, you will send true:
this.setState({ biometryType: true })
this.props.navigation.navigate('OnboardingLast', {
pin: this.props.pin,
biometryStatus: true,
});
You could also do this, since setState can take a callback as param:
this.setState({ biometryType: true }, () => {
this.props.navigation.navigate('OnboardingLast', {
pin: this.props.pin,
biometryStatus: true,
});
})
In your second page this.props.biometryStatus is undefined.
The second argument of getParam is the default value. You should change it like that
const biometryStatus = navigation.getParam('biometryStatus', false);

Passing Prop Into Function

I am just starting to learn reactjs and one of my first use cases is creating a button where depending if user clicks on/off I will make a fetch call and pass this information into one of the variables.I want to eliminate having a function for each type of button. So I figured I could just pass in the value as a prop and use that for the fetch call. You will see this when I try to pass in "this.props.statusnumber".
Unfortunately I get the following error;
Parsing error: this is a reserved word...
Here is my code, any help would be greatly appreciated since I cant find anything online.
import React from 'react';
const API = 'https://use1-wap.tplinkcloud.com/?token=HIDDEN';
let opts = {"method":"passthrough", "set_dev_alias":{"alias":"supercool plug"}, "params": {"deviceId": "HIDDEN", "requestData": "{\"system\": {\"set_relay_state\":{\"state\":0}}}" }};
let headerInfo = {'Content-Type': 'application/json','Version':'1','q': '0.01'};
let statusnumber = {};
export class Name extends React.Component {
constructor(props) {
super(props);
this.state = {
position: "off",
object: [],
};
this.switchStatus = this.switchStatus.bind(this);
this.statusnumber = this.statusnumber.bind(this);
}
switchStatus() {
opts.params.requestData = "{\"system\":{\"set_relay_state\ {\"state\":"+{this.props.statusnumber}+"}}}";
let positionStatus = (this.props.statusnumber === 0) ? "off" : "on";
console.log(this.props.statusnumber);
fetch(API, {
method : 'POST',
headers: headerInfo,
body : JSON.stringify(opts)
})
.then(response => response.json())
.then(data => this.setState({ object: data.object, position: positionStatus}))
};
render() {
const { object } = this.state;
return (
<div>
<button onClick={this.switchStatus} statusnumber={1}>On</button>
<button onClick={this.switchStatus} statusnumber={0}>Off</button>
<p>Current position: {this.state.position}</p>
<p>testing function</p>
</div>
);
}
}

Resources