Reputation: 167
Project: I'm building a Chatbot and I want to render the messages of the Chatbot sequentially with a delay between each other.
Problem: The messages are saved in a state array. So far I was using map
in the JSX of my ChatScreen Component to return a ChatBotMessage component containing the actual message. Since map
doesn't support async behavior (except for resolving all promises with Pomise.all()
which is not helpful in my case), I have tried to use a for..of
loop. The problem here is that I can't return the ChatBotMessage component within the loop, because as soon as I return something the loop stops.
Wanted Behavior: I need to find a way too iterate through the state array and display the latest messages with a delay from within JSX. The state object looks like this:
messages = [
{type: text,
text: 'Chatbot message'
},
{type: 'typing',
value: true
},
{type: text,
text: 'Chatbot message'
},
{type: 'typing',
value: true
}
....
]
Every time the iteration hits an element of type = 'typing' that is set to true, it should delay the next message. How can I achieve this?
EDIT: Added the ChatScreen Component:
export const ChatScreen = ({ navigation }) => {
const [input, setInput] = useState("");
const dispatch = useDispatch();
const messageState: MessagesState = useSelector((state: RootState) => state.messagesState)
const userState: User = useSelector((state: RootState) => state.userState)
const scrollToRef: any = useRef();
const scrollToBottom = () => {
if (scrollToRef.current) {
scrollToRef.current.scrollToEnd({ animated: true })
}
}
useEffect(() => {
if (!messageState.loadedHistory && !messageState.updatingHistory) {
dispatch(requestMessagesHistory(userState.id))
}
}, []);
useEffect(() => {
const onFocus = navigation.addListener('focus', () => {
if (!messageState.loaded && !messageState.updating && messageState.loadedHistory && !messageState.updatingHistory) {
dispatch(requestMessagesInitial(userState.id));
}
});
return onFocus;
}, [navigation]);
return (
<View style={styles.container}>
<ScrollView
ref={scrollToRef}
onContentSizeChange={() => {
if (scrollToRef.current) {
scrollToRef.current.scrollToEnd({ animated: true })
}
}
style={styles.containerInner}>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View style={styles.chatArea}>
{*Here I need to iterate over the message array and return the Chat Messages*}
{messageState.messages.map((item, index, arr) => {
if (item.type === 'text' && item.text !== 'init') {
if (!item.human) {
return <ChatMessageBot index={index} text={item.text} />
} else if (item.human) {
return <ChatMessageHuman index={index} message={item.text} />
} else {
return <ChatMessageBot index={index} text={item.text} />;
}
}
})}
</View>
</TouchableWithoutFeedback>
</ScrollView>
</View>
)
}
}
Upvotes: 1
Views: 1624
Reputation: 45
You're on the right track with the for loop. As you stated, promise.all
isn't really useful as you need to chain the asynchronous waits sequentially.
You can use async
await
syntax to solve this relatively neatly.
Try something like this instead:
(Obviously this code isn't react code, but your issue is more fundamental than react)
// Stub, replace with whatever is appropriate (e.g. React state update, or prop invocation)
const renderMessage = (message) => console.log('Message rendered: ', message);
const executeMessages = async (messages, delay) => {
for(message of messages) {
if (message.type === 'text') {
renderMessage(message.value);
} else if (message.type ==="typing" && message.value===true) {
await new Promise(resolve => setTimeout(resolve, typingDelay)); // this creates a promise which will resolve after the typingDelay has elapsed
}
}
}
executeMessages([
{ type: "text", text: "Chatbot message" },
{ type: "typing", value: true },
{ type: "text", text: "Chatbot message" },
{ type: "typing", value: true },
], 3000); // 3000 is 3 seconds
Upvotes: 1
Reputation: 13912
I'm making this answer to give more context to my comment, which I'll repeat here:
I'm probably not interpreting your question the way you meant it, but i think if you pre-process the array to add a delay: {number}
field to the message objects, and then you make the component that renders each message respect that delay internally, that might solve your problem.
Assuming I am interpreting your comment correctly, you preprocess your array to add a delay
property to the right messages
:
messages = [
{type: text,
text: 'Chatbot message'
},
{type: 'typing',
value: true
},
{type: text,
text: 'Chatbot message'
},
{type: 'typing',
value: true
},
{type: text,
text: 'final message',
delay: 2 // seconds
},
]
And then, I assume your JSX is something like
messages.map(message => <ChatBotMessage message={message} />)
So inside your ChatBotMessage, you create some state, maybe delayed = true
and then a timeout function that will take your delay
value, this.props.message.delay || 0
, and inside the settimeout callback you set the state delayed = false
.
Then in your render function, if delayed is true, return null
.
Upvotes: 0