Nico
Nico

Reputation: 167

How to iterate over an state array in React and conditionally delay the next iteration?

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

Answers (2)

David Coxon
David Coxon

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

TKoL
TKoL

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

Related Questions