The following code is part of a chat application build with react native which is working via a socket.io server.
const Message = ({text, isSent}) => {
return (
<View style={[styles.messageContainer, isSent ? styles.sentMessage : styles.receivedMessage]}>
<Text style={styles.messageText}>{text}</Text>
</View>
)
}
export const ChatScreen = ({ route }) => {
const {control, handleSubmit, reset} = useForm();
const socket = useContext(SocketContext)
const { user } = route.params;
const onSendPress = async(data) => {
const message = data.messageInput
const uuid = await AsyncStorage.getItem('uuid')
if(message === '') return
console.log('Message Send: ' + message + ' | to: ' + user.uuid)
addMessage(message, true);
reset();
socket.emit('send-chat-message', {
message: message,
sender: uuid,
recipient: user.uuid
})
}
socket.on('receive-chat-message', async(data) => {
console.log(data)
if(data.sender !== user.uuid) return
addMessage(data.message, false)
})
const [messages, setMessages] = useState([
{ text: 'Hello! How can I help you today?', isSent: false },
{ text: 'Hi! I was wondering if you could give me some advice.', isSent: true },
{ text: 'Of course! I would be happy to help.', isSent: false },
]);
const scrollViewRef = useRef(null);
const addMessage = (text, isSent) => {
setMessages([...messages, { text, isSent }]);
scrollViewRef.current.scrollToEnd({ animated: true });
};
return (
<View style={styles.background}>
<StatusBar style="auto" />
<ScrollView style={styles.container} ref={scrollViewRef}>
{messages.map((message, index) => (
<Message key={index} text={message.text} isSent={message.isSent} />
))}
</ScrollView>
<KeyboardAvoidingView behavior='padding' keyboardVerticalOffset={55} style={styles.bottomTextInput}>
<Controller
name={'messageInput'}
control={control}
render={({field: {value = '', onChange, onBlur}}) => (
<>
<View style={styles.input}>
<TextInput
style={{paddingHorizontal: 11, paddingTop: 2}}
onChangeText={onChange}
onBlur={onBlur}
value={value}
multiline={true}
fontSize={17}
/>
</View>
<TouchableOpacity onPress={handleSubmit(onSendPress)} style={[styles.sendInput, { backgroundColor: value === '' ? '#cacaca' : '#858AE3'}]}>
<Ionicons
name={'paper-plane'}
size={20}
color={'white'} />
</TouchableOpacity>
</>
)}
/>
</KeyboardAvoidingView>
</View>
)
}
Now everything is working quite well, the only problem I'm running into is that somehow the data I receive with the receive-chat-message event is doubling each time a new message comes in. To make it visual, this is my console after three different messages:
{"message": "First Message", "recipient": "7fa68f52-ad00-4bdd-9cfc-2af2e15d8d8d", "sender": "2bfe33f0-4882-4ff6-a9ac-4356cd3e7a66"}
{"message": "Second Message", "recipient": "7fa68f52-ad00-4bdd-9cfc-2af2e15d8d8d", "sender": "2bfe33f0-4882-4ff6-a9ac-4356cd3e7a66"}
{"message": "Second Message", "recipient": "7fa68f52-ad00-4bdd-9cfc-2af2e15d8d8d", "sender": "2bfe33f0-4882-4ff6-a9ac-4356cd3e7a66"}
{"message": "Third Message", "recipient": "7fa68f52-ad00-4bdd-9cfc-2af2e15d8d8d", "sender": "2bfe33f0-4882-4ff6-a9ac-4356cd3e7a66"}
{"message": "Third Message", "recipient": "7fa68f52-ad00-4bdd-9cfc-2af2e15d8d8d", "sender": "2bfe33f0-4882-4ff6-a9ac-4356cd3e7a66"}
{"message": "Third Message", "recipient": "7fa68f52-ad00-4bdd-9cfc-2af2e15d8d8d", "sender": "2bfe33f0-4882-4ff6-a9ac-4356cd3e7a66"}
{"message": "Third Message", "recipient": "7fa68f52-ad00-4bdd-9cfc-2af2e15d8d8d", "sender": "2bfe33f0-4882-4ff6-a9ac-4356cd3e7a66"}
I checked if the server is really just getting one request at a time and it is like this. The server is only getting one request and is also only emitting one request to the client who is receiving the message.
I appreciate any help since the problem is literally bugging me out.
Most likely error is due to you are calling socket.on('receive-chat-message', ...) directly in component body, that means that every time component is rerendered - this line is executed, so you have multiple listeners in same component executing same code on this event, as you can see - every time you call setMessages - component is rerendered, additional listener is added and you are getting 1 more duplicate.
Solution: Wrap it with useEffect:
useEffect(() => {
if (!user?.uuid) return;
const fn = async (data) => {
console.log(data);
if (data.sender !== user.uuid) return;
addMessage(data.message, false);
};
socket.on("receive-chat-message", fn);
return () => {
socket.off("receive-chat-message", fn);
};
}, [socket, user?.uuid]);
Possible error #2 - setMessages([...messages, { text, isSent }]);. Here you can get errors due to closures, prefered to use functional state setter:
setMessages(messages => [...messages, { text, isSent }]);
Related
I am using React as the frontend and Flask as the backend.
Today, the frontend needs to use the response from the backend in the user interface, which will be stored as a dialog using setDialog and rendered in the UI.
However, an error "Uncaught (in promise) Error: Too many re-renders. React limits the number of renders to prevent an infinite loop" keeps occurring.
I have tried using useEffect to fetch the dialog and also setting up a button to avoid repeated rendering, but neither method has worked.
Using useEffect:
const [dialog, setDialog] = useState([]);
useEffect(() => {
const handleAddDialog = async () => {
const url = `http://127.0.0.1:5000/question_hints_dialog/ww/dd/C1_P1`;
const response = await fetch(url);
const data = await response.json();
console.log("data", data);
setDialog(data);
};
handleAddDialog();
}, []);
Using button mode:
const handleAddDialog = async () => {
const url = `http://127.0.0.1:5000/question_hints_dialog/ww/dd/C1_P1`;
const response = await fetch(url);
dialogs = await response.json();
setDialog(dialogs)
};
return(
<Button onClick={()=>handleAddDialog()}>Start</Button>
)
I would like to know how to solve this issue. Thank you.
<List ref = {(ref)=>setScrollbarRef(ref)} className={classes.messageArea} style={{maxHeight: 500, overflow: 'auto'}}>
<Button onClick={()=>handleAddDialog()}>開始</Button>
{dialog && dialog.map((msg, idx) => {
console.log("detail",msg.detail)
let linkComponent = null;
if(msg.id === 1){
linkComponent =<></>;
}
else if (msg.id === 2) {
setHintsCount(1)
linkComponent = importConcept
//<Link href="#" onClick={() => handleProcessStage(false, "開始 PyTutor")}>開始 PyTutor</Link>;
} else if (msg.id === 3) {
linkComponent = <Link href="#" onClick={() => handleConcept(false)}>GOGo</Link>;
}
const detail_update = <>{msg.detail}<br/>{linkComponent}</>
return (
<React.Fragment key={idx}>
<ListItem key={idx} className = {msg.from === 'student'? classes.stuPos:classes.tutorPos}>
{msg.detail && (
<Grid container className = {msg.from === 'student'?classes.stuMsg:classes.tutorMsg}>
<Grid item={true} xs style={{display:'flex'}}>
<ListItemText primary= {
detail_update
}/>
</Grid>
<Grid item={true} xs={12}>
<ListItemText className={msg.from === 'student'? classes.stuPos:classes.tutorPos} secondary={currentTime}></ListItemText>
</Grid>
</Grid>
)}
</ListItem>
</React.Fragment>
);
})}
</List>
Here is now my frontend useEffect code:
useEffect(() => {
const fetchData = async () => {
const options = await getStoredOptions();
setOptions(options);
setOptionsLoaded(true);
};
const handleScrollbar = () => {
if (scrollbarRef) {
new PerfectScrollbar(scrollbarRef, {
wheelSpeed: 2,
wheelPropagation: true,
minScrollbarLength: 20
});
}
};
if (!optionsLoaded) {
fetchData();
}
handleScrollbar();
if (hint) {
console.log("Hint updated: ", hint);
}
if (optionsLoaded && options?.student_name && options?.student_id) {
console.log("initial");
setIsNew(true);
// do something here...
setIsNew(false);
}
}, [scrollbarRef, isSolved, optionsLoaded, hint, pesudo, cloze, originCode, advCode, count, options]);
Backend code:
#app.route('/question_hints_dialog/<string:stu_name>/<string:stu_id>/<string:hint_id>')
def generate_question_hints_dialog(stu_name, stu_id, hint_id):
name = userInfo.student_name
stu_id =userInfo.sudent_id
dialog = []
# dialog.append({"id": 1, "detail": f"... {stu_name} ... {stu_id}", "from": 'student'})
dialog.append({"id": 1, "detail": f"...,{stu_name}! ... " , "from": 'tutor' })
dialog.append({"id": 2, "detail": f"...", "from": 'tutor'})
dialog.append({"id": 3, "detail": "..." , "from": 'tutor' })
dialog.append({"id": 4, "detail": "..." , "from": 'tutor' })
dialog.append({"id": 5, "detail": "..." , "from": 'tutor' })
dialog.append({"id": 6, "detail": "..." , "from": 'tutor' })
dialog.append({"id": 7, "detail": "..." , "from": 'tutor' })
dialog.append({"id": 8, "detail": "..." , "from": 'tutor' })
return jsonify(dialog)
I tried many method to solve this issue but they couldn't work.
Finally, I found that
const [dialog, setDialog] = useState<{ id: number; detail?: JSX.Element; from: string }[]>([]);
The problem is that detail is initialized as JSX.Element
When React reloads, it would keep to set detail as JSX.Element, but JSX keeps changing. So, the re-render problem happens.
Now I change to
const [dialog, setDialog] = useState<{ id: number; detail: string; from: string }[]>([]);
and it figures out.
Share here and thanks for your concern.
If anything I realize wrong, feel free to let me know.
I have the following JSON which I want to map to MUI Cards. I am not getting any error messages but nothing is being displayed. The console.log(questionGroups) only displays the JSON after changing some unrelated code to cause a live reload.
const [questionGroups, setQuestionGroups] = useState("");
const fetchQuestionGroups= async () => {
setQuestionGroups(
await fetch(`API_LINK`).then((response) => response.json())
);
console.log(questionGroups);
};
useEffect(() => {
fetchQuestionGroups();
}, []);
...
<Box className={classes.cards}>
{questionGroups?.displaygroups?.IntakeQuestion?.map((group, groupIndex) => {
return (
<Card className={classes.card1}>
<CardHeader title={group.GroupName} />
</Card>
);
})}
</Box>
This is a sample of my JSON:
{
"displaygroups": {
"IntakeQuestions": [
{
"GroupId": 11,
"GroupName": "Group 1",
"SizingId": null,
"OwnerName": "Jane Doe",
"Questions": 0,
"Answered": null,
"Validated": null
}
]
}
}
Use && instead of ?
<Box className={classes.cards}>
{questionGroups &&
questionGroups.displaygroups &&
questionGroups.displaygroups.IntakeQuestions.map((group, groupIndex) => {
return (
<Card className={classes.card1}>
<CardHeader title={group.GroupName} />
</Card>
);
})}
</Box>
You need to set the state once the data is available.
const fetchQuestionGroups= async () => {
const data = await fetch(`API_LINK`)
const temp = response.json()
setQuestionGroups(temp);
console.log(questionGroups);
};
Am new to webRTC and am trying to create a react native app with video calling functionality using this tutorial here as an example to follow https://dipanshkhandelwal.medium.com/video-calling-using-firebase-and-webrtc-14cc2d4afceb
However i keep getting this error on iOS and on android the app just closes once i try to join a call. The error i get on iOS says:
JSON value '{
}' of type NSMutableDictionary cannot be converted to .sdp must not be null
+[RCTConvert(WebRTC) RTCSessionDescription:]
RCTConvert+WebRTC.m:22
__41-[RCTModuleMethod processMethodSignature]_block_invoke_16
-[RCTModuleMethod invokeWithBridge:module:arguments:]
facebook::react::invokeInner(RCTBridge*, RCTModuleData*, unsigned int, folly::dynamic const&, int, (anonymous namespace)::SchedulingContext)
facebook::react::RCTNativeModule::invoke(unsigned int, folly::dynamic&&, int)::$_0::operator()() const
invocation function for block in facebook::react::RCTNativeModule::invoke(unsigned int, folly::dynamic&&, int)
_dispatch_call_block_and_release
_dispatch_client_callout
_dispatch_lane_serial_drain
_dispatch_lane_invoke
_dispatch_workloop_worker_thread
_pthread_wqthread
start_wqthread
in metro this comes up as an error:
Possible Unhandled Promise Rejection (id: 0):
Object {
"message": "SessionDescription is NULL.",
"name": "SetRemoteDescriptionFailed",
}
A sample of the offer created in firstore in the respective room is below. Am suspecting the sdp might be invalid or something.
_sdp:
v=0o=- 765269967391877801 2 IN IP4 127.0.0.1s=-t=0 0a=group:BUNDLE audio videoa=msid-semantic: WMS a3a6a14c-980f-4052-a881-e53290c2e9a8m=video 100c=IN IP4 0.0.0.0a=rtcp:9 IN IP4 0.0.0.0a=ice-ufrag:qfEea=ice-pwd:aMtS7ouM3ykOCV6z7jx0hhlSa=ice-options:trickle renominationa=fingerprint:sha-256 01:13:79:2E:BA:E3:67:9A:77:66:90:00:D7:62:BA:31:2C:48:FC:EB:8C:21:22:7E:5B:9D:0E:71:82:5F:63:5Aa=setup:actpassa=mid:audioa=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-levela=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-timea=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01a=sendrecva=rtcp-muxa=rtpmap:111 opus/48000/2a=rtcp-fb:111 transport-cca=fmtp:111 minptime=10;useinbandfec=1a=rtpmap:103 ISAC/16000a=rtpmap:9 G722/8000a=rtpmap:102 ILBC/8000a=rtpmap:0 PCMU/8000a=rtpmap:8 PCMA/8000a=rtpmap:105 CN/16000a=rtpmap:13 CN/8000a=rtpmap:110 telephone-event/48000a=rtpmap:113 telephone-event/16000a=rtpmap:126 telephone-event/8000a=ssrc:866776128 cname:OXEDL0qxAsrhPmB7a=ssrc:866776128 msid:a3a6a14c-980f-4052-a881-e53290c2e9a8 256a9ac0-8163-45b5-ae52-0ece88651c51a=ssrc:866776128 mslabel:a3a6a14c-980f-4052-a881-e53290c2e9a8a=ssrc:866776128 label:256a9ac0-8163-45b5-ae52-0ece88651c51m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 127c=IN IP4 0.0.0.0a=rtcp:9 IN IP4 0.0.0.0a=ice-ufrag:qfEea=ice-pwd:aMtS7ouM3ykOCV6z7jx0hhlSa=ice-options:trickle renominationa=fingerprint:sha-256 01:13:79:2E:BA:E3:67:9A:77:66:90:00:D7:62:BA:31:2C:48:FC:EB:8C:21:22:7E:5B:9D:0E:71:82:5F:63:5Aa=setup:actpassa=mid:videoa=extmap:14 urn:ietf:params:rtp-hdrext:toffseta=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-timea=extmap:13 urn:3gpp:video-orientationa=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delaya=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-typea=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timinga=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-spacea=sendrecva=rtcp-muxa=rtcp-rsizea=rtpmap:96 VP8/90000a=rtcp-fb:96 goog-remba=rtcp-fb:96 transport-cca=rtcp-fb:96 ccm fira=rtcp-fb:96 nacka=rtcp-fb:96 nack plia=rtpmap:97 rtx/90000a=fmtp:97 apt=96a=rtpmap:98 VP9/90000a=rtcp-fb:98 goog-remba=rtcp-fb:98 transport-cca=rtcp-fb:98 ccm fira=rtcp-fb:98 nacka=rtcp-fb:98 nack plia=rtpmap:99 rtx/90000a=fmtp:99 apt=98a=rtpmap:100 red/90000a=rtpmap:101 rtx/90000a=fmtp:101 apt=100a=rtpmap:127 ulpfec/90000a=ssrc-group:FID 2850853975 3259115858a=ssrc:2850853975 cname:OXEDL0qxAsrhPmB7a=ssrc:2850853975 msid:a3a6a14c-980f-4052-a881-e53290c2e9a8 3f3952f6-cd3d-4d3e-a2dc-81eda77cf516a=ssrc:2850853975 mslabel:a3a6a14c-980f-4052-a881-e53290c2e9a8a=ssrc:2850853975 label:3f3952f6-cd3d-4d3e-a2dc-81eda77cf516a=ssrc:3259115858 cname:OXEDL0qxAsrhPmB7a=ssrc:3259115858 msid:a3a6a14c-980f-4052-a881-e53290c2e9a8 3f3952f6-cd3d-4d3e-a2dc-81eda77cf516a=ssrc:3259115858 mslabel:a3a6a14c-980f-4052-a881-e53290c2e9a8a=ssrc:3259115858 label:3f3952f6-cd3d-4d3e-a2dc-81eda77cf516
Tried everything but can't seem to figure it out. The callee collection also never gets created in the respective rooms in firestore database. Any help with this would be appreciated.
I guess you are trying to use firebase as a signalling medium and want to use react-native-webrtc for the video calling.
Here is the sample code I have for the same solution with the latest libraries and react-native version.
Firebase Installation React Native.
Just set up ios and android using this above link and then use the below code for reference.
import React, {useEffect, useState} from 'react';
import {Modal, TextInput, ToastAndroid, View} from 'react-native';
import {ColoredButton} from '../app/common/commonViews';
import {
mediaDevices,
MediaStream,
RTCIceCandidate,
RTCPeerConnection,
RTCSessionDescription,
RTCView,
} from 'react-native-webrtc';
import firestore from '#react-native-firebase/firestore';
const Home = () => {
const [localStream, setLocalStream] = useState(null);
const [remoteStream, setRemoteStream] = useState(new MediaStream());
const [modalVisible, setModalVisible] = useState(false);
const [roomName, setRoomName] = useState('2277');
let peerConnection;
const configuration = {
iceServers: [
{urls: 'stun:stun.services.mozilla.com'},
{urls: 'stun:stun.l.google.com:19302'},
],
};
let isFront = true;
useEffect(() => {
mediaDevices.enumerateDevices().then((sourceInfos) => {
console.log(sourceInfos);
let videoSourceId;
for (let i = 0; i < sourceInfos.length; i++) {
const sourceInfo = sourceInfos[i];
if (
sourceInfo.kind == 'videoinput' &&
sourceInfo.facing == (isFront ? 'front' : 'environment')
) {
videoSourceId = sourceInfo.deviceId;
}
}
mediaDevices
.getUserMedia({
audio: true,
video: {
width: 640,
height: 480,
frameRate: 30,
facingMode: isFront ? 'user' : 'environment',
deviceId: videoSourceId,
},
})
.then((stream) => {
setLocalStream(stream);
// Got stream!
})
.catch((error) => {
// Log error
});
});
}, [isFront]);
async function createRoom() {
const roomRef = firestore().collection('rooms').doc(roomName);
console.log(roomRef);
console.log('Create PeerConnection with configuration: ', configuration);
peerConnection = new RTCPeerConnection(configuration);
peerConnection.addStream(localStream);
// Code for collecting ICE candidates below
const callerCandidatesCollection = roomRef.collection('callerCandidates');
// Code for collecting ICE candidates above
peerConnection.onicecandidate = function (event) {
if (!event.candidate) {
console.log('Got final candidate!');
return;
}
console.log('Got candidate: ', event.candidate);
callerCandidatesCollection.add(event.candidate.toJSON());
};
peerConnection.onicegatheringstatechange = () => {
console.log(
`ICE gathering state changed: ${peerConnection.iceGatheringState}`,
);
};
peerConnection.onconnectionstatechange = () => {
console.log(`Connection state change: ${peerConnection.connectionState}`);
};
peerConnection.onsignalingstatechange = () => {
console.log(`Signaling state change: ${peerConnection.signalingState}`);
};
peerConnection.oniceconnectionstatechange = () => {
console.log(
`ICE connection state change: ${peerConnection.iceConnectionState}`,
);
};
console.log('Created offer:', offer);
// Code for creating a room below
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
const roomWithOffer = {
offer: {
type: offer.type,
sdp: offer.sdp,
},
};
await roomRef.set(roomWithOffer);
console.log(`New room created with SDP offer. Room ID: ${roomRef.id}`);
ToastAndroid.show(
`New room created with SDP offer. Room ID: ${roomRef.id}`,
ToastAndroid.SHORT,
);
// Code for creating a room above
peerConnection.onaddstream = (event) => {
setRemoteStream(event.stream);
};
// Listening for remote session description below
roomRef.onSnapshot(async (snapshot) => {
const data = snapshot.data();
if (!peerConnection.currentRemoteDescription && data && data.answer) {
console.log('Got remote description: ', data.answer);
const rtcSessionDescription = new RTCSessionDescription(data.answer);
await peerConnection.setRemoteDescription(rtcSessionDescription);
}
});
// Listening for remote session description above
// Listen for remote ICE candidates below
roomRef.collection('calleeCandidates').onSnapshot((snapshot) => {
snapshot.docChanges().forEach(async (change) => {
if (change.type === 'added') {
let data = change.doc.data();
console.log(`Got new remote ICE candidate: ${JSON.stringify(data)}`);
await peerConnection.addIceCandidate(new RTCIceCandidate(data));
}
});
});
// Listen for remote ICE candidates above
}
async function joinRoomById(roomId) {
const roomRef = firestore().collection('rooms').doc(`${roomId}`);
const roomSnapshot = await roomRef.get({
source: 'server',
});
console.log('Got room:', roomSnapshot.exists);
if (roomSnapshot.exists) {
console.log('Create PeerConnection with configuration: ', configuration);
peerConnection = new RTCPeerConnection(configuration);
peerConnection.addStream(localStream);
// Code for collecting ICE candidates below
const calleeCandidatesCollection = roomRef.collection('calleeCandidates');
peerConnection.onicecandidate = function (event) {
if (!event.candidate) {
console.log('Got final candidate!');
return;
}
console.log('Got candidate: ', event.candidate);
calleeCandidatesCollection.add(event.candidate.toJSON());
};
peerConnection.onicegatheringstatechange = () => {
console.log(
`ICE gathering state changed: ${peerConnection.iceGatheringState}`,
);
};
peerConnection.onconnectionstatechange = () => {
console.log(
`Connection state change: ${peerConnection.connectionState}`,
);
};
peerConnection.onsignalingstatechange = () => {
console.log(`Signaling state change: ${peerConnection.signalingState}`);
};
peerConnection.oniceconnectionstatechange = () => {
console.log(
`ICE connection state change: ${peerConnection.iceConnectionState}`,
);
};
// Code for collecting ICE candidates above
peerConnection.onaddstream = (event) => {
setRemoteStream(event.stream);
};
// Code for creating SDP answer below
const offer = roomSnapshot.data().offer;
console.log('Got offer:', offer);
await peerConnection.setRemoteDescription(
new RTCSessionDescription(offer),
);
const answer = await peerConnection.createAnswer();
console.log('Created answer:', answer);
await peerConnection.setLocalDescription(answer);
const roomWithAnswer = {
answer: {
type: answer.type,
sdp: answer.sdp,
},
};
await roomRef.update(roomWithAnswer);
// Code for creating SDP answer above
// Listening for remote ICE candidates below
roomRef.collection('callerCandidates').onSnapshot((snapshot) => {
snapshot.docChanges().forEach(async (change) => {
if (change.type === 'added') {
let data = change.doc.data();
console.log(
`Got new remote ICE candidate: ${JSON.stringify(data)}`,
);
await peerConnection.addIceCandidate(new RTCIceCandidate(data));
}
});
});
// Listening for remote ICE candidates above
}
}
return (
<View style={{flex: 1}}>
<Modal
visible={modalVisible}
transparent={true}
style={{justifyContent: 'center'}}>
<View
style={{
height: 100,
padding: 20,
width: '80%',
alignSelf: 'center',
justifyContent: 'center',
backgroundColor: 'white',
}}>
<TextInput
value={roomName}
onChangeText={(text) => {
setRoomName(text);
}}
placeholder={'Enter text'}
/>
<View
style={{
flexDirection: 'row',
justifyContent: 'space-evenly',
}}>
<ColoredButton
title="close"
onPress={() => {
setModalVisible(!modalVisible);
}}
/>
<ColoredButton
title="create"
onPress={() => {
setModalVisible(!modalVisible);
}}
/>
<ColoredButton
title="join"
onPress={() => {
setModalVisible(!modalVisible);
}}
/>
</View>
</View>
</Modal>
<RTCView
streamURL={localStream ? localStream.toURL() : null}
style={{flex: 1, margin: 20, backgroundColor: 'black'}}
/>
<RTCView
style={{
flex: 1,
marginLeft: 20,
marginRight: 20,
backgroundColor: 'black',
}}
streamURL={remoteStream ? remoteStream.toURL() : null}
/>
<TextInput value={roomName} onChangeText={setRoomName} />
<View
style={{
flexDirection: 'row',
justifyContent: 'space-evenly',
width: '100%',
padding: 20,
}}>
<ColoredButton
title="Create"
onPress={() => {
createRoom().then((r) => {});
}}
/>
<ColoredButton
title="Join"
onPress={() => {
joinRoomById(roomName).then((r) => {});
}}
/>
</View>
</View>
);
};
export default Home;
I am using Flatlist to show a list of Date. I have created a component DateList to view the list of item.
I have already called an API apiFreelancerDate. However, currently I am trying to call another function through the component (which I will later use it for second API call).
Date screen:
apiFreelancerDate = () => {
let self = this;
AsyncStorage.getItem('my_token').then((keyValue) => {
axios({
method: 'get',
url: Constants.API_URL + 'user_m/confirmation_date/',
params: {
freelancer_id: self.props.navigation.state.params.fr_id,
},
responseType: 'json',
headers: {
'X-API-KEY': Constants.API_KEY,
'Authorization': keyValue,
},
})
.then(function (response) {
self.setState({
dataSource: response.data.data,
isLoading: false,
});
})
.catch(function (error) {
console.log('Error Response: ', error.response);
});
}, (error) => {
console.log('Error', error) //Display error
});
}
//test function call
testFunc = () => {
console.log('test function called');
}
//test function call
viewFreelancerDate() {
const { dataSource } = this.state;
return (
<View>
{<FlatList
data={dataSource}
keyExtractor={({ id }, index) => index.toString()}
renderItem={({ item }) => <DateList {...item} functionCall={this.testFunc}/>}
/>}
</View>
);
}
DateList component:
<TouchableOpacity
style={{ flex: 1 }}
onPress={() => this.props.navigation.navigate('masterFreelancerTime')}
>
<Text style={styles.textStartDate}>{this.props.date_start}</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.activeBorder}
onPress={() => {
this.props.functionCall;
const newStatus = !this.state.status;
this.setState({
status: newStatus,
});
console.log('status: ', this.state.status);
console.log('Date: ', this.props.date_start);
}}
>
<Text style={styles.active_inactive}>{this.state.status ? "INACTIVE" : "ACTIVE"}</Text>
</TouchableOpacity>
I am very new to this so any help would be appreciated and I don't know if it's the right way to do it.
My main purpose is that I want to add a function call that will call an API which will show the state "INACTIVE" : "ACTIVE" based on what's in the database. And upon pressing/tapping, the "INACTIVE" : "ACTIVE" will toggle and at the same time it'll update it's value in the database.
You are mostly on right track. Although you may want to keep few things in mind.
e.g.
<TouchableOpacity
style={styles.activeBorder}
onPress={() => {
this.props.functionCall;
const newStatus = !this.state.status;
this.setState({
status: newStatus,
});
console.log('status: ', this.state.status);
console.log('Date: ', this.props.date_start);
}}
>
This is an overoptimisic assignment to state.status before api call finishes. What if the api call fails, in that case, your status is not in-sync with database. You can update the state without waiting for API to return the response, but you should update the state again in case API fails.
If you don't need optimistic state update, then you can do something like following:
Date screen:
//test function call
testFunc = () => {
console.log('test function called');
const dataSource = makeAPiCallAndGetUpdatedApiSource();
this.setState({ dataSource });
}
//test function call
viewFreelancerDate() {
const { dataSource } = this.state;
return (
<View>
{<FlatList
data={dataSource}
keyExtractor={({ id }, index) => index.toString()}
renderItem={({ item }) => <DateList {...item} functionCall={this.testFunc}/>}
/>}
</View>
);
}
DateList component:
constructor(props) {
this.state = {
status: props.status
};
}
...
componentDidUpdate() {
this.setState({ status: this.props.status })
}
...
<TouchableOpacity
style={styles.activeBorder}
onPress={() => {
this.props.functionCall();
}}
>
This way, after the api call, state of date screen will be updated ... which in turn will update DateList component, which will update the state of component.
I am a complete noob to React Native and I am just trying to get hang of it.
I have a simple Login screen which I have developed using Container/Presentation component concept. My presentation component only has render function that renders three two TextInput and a Button.
Rather than displaying errors using Toasts I want to display errors below the TextInput itself. So, what I have done is added Text element below the TextInput. Something like below.
<TextInput placeholder="Email"></TextInput>
<Text ref="emailErrors"></Text>
By default, Text with ref emailErrors is hidden. But when the focus shifts from Email TextInput and if email is invalid for some reason I want to generate a simple error and set it as text for Text element with ref emailErrors.
Now, I understand that I will have to write my logic in container components and pass it as prop to presentation component. But what I am unable to understand is how to trigger the setting of error text and displaying the Text element.
UPDATE:
My Presentation Component:
class LoginForm extends Component {
constructor(props) {
super(props);
}
render() {
return (
<KeyboardAvoidingView
style={styles.loginFormContainer}>
<ScrollView
scrollEnabled={true}
enableOnAndroid={true}
enableAutomaticScroll={true}
keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="always"
resetScrollToCoords={{ x: 0, y: 0 }}
showsVerticalScrollIndicator={false}
showsHorizontalScrollIndicator={false}>
<View style={styles.loginForm}>
<Image
source={require("../../../assets/images/logos/logo.png")}
style={styles.logoImage}></Image>
<View style={styles.textInputWithIcon}>
<Image
style={styles.textInputIcon}
source={require("../../../assets/images/icons/email.png")}
></Image>
<View style={styles.textField}>
<TextInput name="email"
ref="email"
placeholder="Email"
blurOnSubmit={false}
returnKeyType={"next"}
underlineColorAndroid={COLORS.red}
onSubmitEditing={() => this.refs.password.focus()}
style={[GLOBALSTYLES.textInput, styles.textInput]}
onChangeText={(text) => this.props.onEmailTextChanged(text)}
>
</TextInput>
<Text
ref="email">
</Text>
</View>
</View>
<View style={styles.textInputWithIcon}>
<Image
style={styles.textInputIcon}
source={require("../../../assets/images/icons/locked.png")}
></Image>
<View style={styles.textField}>
<TextInput name="password"
ref="password"
blurOnSubmit={false}
placeholder="Password"
secureTextEntry={true}
returnKeyType={"next"}
style={styles.textInput}
underlineColorAndroid={COLORS.red}
onSubmitEditing={() => Keyboard.dismiss()}
style={[GLOBALSTYLES.textInput, styles.textInput]}
onChangeText={(text) => this.props.onPasswordTextChanged(text)}
onBlur={() => this.props.onPasswordTextBlurred()}
></TextInput>
<Text
ref="password">
</Text>
</View>
</View>
<TouchableOpacity
activeOpacity={0.5}
onPress={() => { this.props.onLoginPressed() }}
style={styles.loginButton}
underlayColor={COLORS.white}>
<Text style={styles.loginButtonText}>Login</Text>
</TouchableOpacity>
</View>
</ScrollView>
</KeyboardAvoidingView>
)
};
};
export default LoginForm;
My Container Component:
class Login extends Component {
static navigationOptions = ({ navigation }) => ({
"title": "Login"
});
constructor(props) {
super(props);
this.state = {}
}
// onLoginPressed will trigger the authentication workflow with the remote server.
onLoginPressed() {
const { isUserLoggedIn, email, password } = this.state;
if (this.state.isEmailValid && this.state.isPasswordValid) {
axios.post(CONFIGURATION.LOGIN_URL, {
username: email,
password: password
}).then(response => {
const navigationParams = {
baseUrl: response.data.url,
token: response.data.token,
username: email
}
this.props.dispatch(loginSuccess(navigationParams));
// Adding retrieved values to AsyncStorage
AsyncStorage.multiSet(
[
[IS_USER_LOGGED_IN, "YES"],
[USER, email],
[TOKEN, response.data.token],
[BASE_URL, response.data.url]
],
() => {
this.props.navigation.navigate("WebApp", navigationParams);
});
}).catch(error => {
console.error(error);
ToastAndroid.show("Authentication Failed", ToastAndroid.SHORT);
});
}
}
// Updating the state key email
onEmailTextChanged(text) {
this.setState({ "email": text });
}
// Updating the state key password
onPasswordTextChanged(text) {
this.setState({ "password": text });
}
onEmailTextBlurred() {
var text = this.state.email;
console.warn(text);
if (text == undefined || text.trim().length == 0) {
this.setState({ "isEmailValid": false });
this.setState({ "emailErrorMessage": "Email cannot be empty" });
}
else {
var regex = /^(([^<>()\[\]\\.,;:\s#"]+(\.[^<>()\[\]\\.,;:\s#"]+)*)|(".+"))#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
var isEmailValid = regex.test(text);
if (!isEmailValid) {
this.setState({ "isEmailValid": false });
this.setState({ "emailErrorMessage": "Email is incorrect." });
}
else {
this.setState({ "isEmailValid": true });
}
}
}
onPasswordTextBlurred() {
var text = this.state.password;
if (text == undefined || text.trim().length == 0) {
this.setState({ "isPasswordValid": false });
this.setState({ "passwordErrorMessage": "Password cannot be empty" });
}
else {
this.setState({ "isPasswordValid": true });
}
}
// rendering the LoginForm (presentational component) corresponding to this container component
render() {
return (
<LoginForm
onLoginPressed={() => this.onLoginPressed()}
onEmailTextChanged={(text) => this.onEmailTextChanged(text)}
onPasswordTextChanged={(text) => this.onPasswordTextChanged(text)}
onEmailTextBlurred={() => this.onEmailTextBlurred()}
onPasswordTextBlurred={() => this.onPasswordTextBlurred()}
/>
)
}
}
const mapStateToProps = (state) => {
return state;
}
const mapDispatchToProps = (dispatch) => {
const boundActionCreators = bindActionCreators(loginSuccess, dispatch);
return { ...boundActionCreators, dispatch };
}
export default connect(mapStateToProps, mapDispatchToProps)(Login);
You can use a flag in state of the component to check if invalid input is given by checking the status of the flag.
For example:
emailValidationRegex(v){
// use regex to check if email is valid and return true if email is valid, false if email is invalid
}
<TextInput
placeholder="Email"
onChange={(v)=>{this.setState({
email: v,
isEmailInvalid: !emailValidationRegex(v)
})}} >
</TextInput>
{this.state.isEmailInvalid && <Text>Sorry! Invalid Email</Text>}
Explanation:
isEmailInvalid is keeping the status of given email address if it is valid or not. Depending on it's status the following Error is shown conditionally.
Update:
In the onEmailTextChanged method in your container component update another state to hold if the email is valid or not:
onEmailTextChanged(text) {
this.setState({ "email": text, isEmailInvalid: !emailValidationRegex(text) });
}
Then pass this.state.isEmailInvalid in the props sent to the presentational component. Show error conditionally in the presentational component then.
render method of container component:
render() {
return (
<LoginForm
onLoginPressed={() => this.onLoginPressed()}
onEmailTextChanged={(text) => this.onEmailTextChanged(text)}
isEmailInvalid={this.state.isEmailInvalid}
onPasswordTextChanged={(text) => this.onPasswordTextChanged(text)}
onEmailTextBlurred={() => this.onEmailTextBlurred()}
onPasswordTextBlurred={() => this.onPasswordTextBlurred()}
/>
)
Now you are able to use this.props.isEmailInvalid in the presentational component