Search code examples
javascriptarrays

Executing function inside {#each} loop uses values in reversed order, sveltekit


I'm looping over some messages in a chat app and adding date division element that shows the date of sending of the messages under it, but when I have multiple messages sent on the same day, the divider doesn't work as expected and goes between messages that should be in the same date group. I have a function that gets the date and formats it, then returns an object with the formatted date(dd-mm-yyyy) and time(hour-minute AM/PM). When I add a function inside the {#each} loop to console.log the result of the formatDate(), it shows in the console in reversed order. For example, if I have a message sent at 8:35 AM, and a second one sent at 8:37 AM, in the console it shows the 8:37 first, then the 8:35. The array that contains the messages is sorted in chronological order, and when I display the indexes of elements in the array or log them to the console, they are in order. I think there might be something about some asynchronous stuff going on that makes the loop reference values in a different order.

let datesGroup: string[] = [];

// Function to get the moment of the day and of the year when the message was sent
function formatDate(dateStr: string, addToArray: boolean = false): MessageDate {
    if (!dateStr) return { ofYear: '', ofDay: '' };

    // Set pointers in time
    const date = new Date(dateStr);
    const todayDate = new Date();
    const { hour, minute, meridian, day, month, year } = getDateValues(date);
    const yearDate = `${day}-${month}-${year}`;
    const time = `${hour}:${minute} ${meridian}`;
    const today = `${getDateValues(todayDate).day}-${getDateValues(todayDate).month}-${getDateValues(todayDate).year}`;
    let isNewDate = false;

    // Check if a message is part of a different date group
    if (datesGroup.indexOf(yearDate) === -1) isNewDate = true;

    // Add the date divider to the array
    if (addToArray && isNewDate) datesGroup = [...datesGroup, yearDate];

    return {
        isNewDate: isNewDate,
        ofDay: time,
        ofYear: today === yearDate ? 'Today' : yearDate
    };
}

function getDateValues(date: Date) {
    return {
        minute: date.getMinutes() > 9 ? date.getMinutes() : '0' + date.getMinutes(),
        hour: date.getHours() > 12 ? date.getHours() - 12 : date.getHours(),
        day: date.getDate() > 9 ? date.getDate() : '0' + date.getDate(),
        month: date.getMonth() > 9 ? date.getMonth() : '0' + date.getMonth(),
        year: date.getFullYear(),
        meridian: date.getHours() > 12 ? 'PM' : 'AM'
    };
}

{#each messages as message (message.id)}
    {#if formatDate(String(message.sentat), false).isNewDate}
        <div class="date-display">
            {formatDate(String(message.sentat), true).ofYear}
        </div>
    {/if}
    <div>
        {#if message.from != $page.data.USER.id}
            <img
                class="msg-profile-picture"
                alt="pfp"
                src={getProfilePicture(message.from, currentRoomMembers)}
            />
        {/if}
        <div class="msg-content" class:sent-by-me={$page.data.USER.id === message.from}>
            {#if message.from !== $page.data.USER.id}
                <span
                    ><a href="/profiles/{message.from}">{GetUsername(message.from, currentRoomMembers)}</a
                    ></span
                >
            {/if}
            <div class:shortened={message.text.length > 1400 && !message.shortened}>
                {message.text.length < 1400
                    ? message.text
                    : message.text.split('').slice(0, 1400).join('')}
                {#if message.text.length > 1400}
                    <button type="button" class="show-more">Show more</button>
                {/if}
            </div>
            <span>{formatDate(String(message.sentat), false).ofDay}</span>
        </div>
    </div>
{/each}

I tried referencing individual messages by their index, but it still was reversed


Solution

  • The issue you're facing is likely due to the asynchronous nature of the rendering process in Svelte. When you execute a function inside the {#each} loop, the function is called for each iteration of the loop, but the results may not be returned in the same order as the loop iteration.

    To address this, you can try the following approach:

    1. Precompute the date groups: Instead of computing the date groups inside the {#each} loop, precompute the date groups before the loop and store them in a separate array. This way, you can reference the date groups in the correct order during the loop.
    // Precompute the date groups
    let datesGroup: string[] = [];
    messages.forEach((message) => {
        const { ofYear } = formatDate(String(message.sentat), true);
        if (!datesGroup.includes(ofYear)) {
            datesGroup = [...datesGroup, ofYear];
        }
    });
    
    {#each messages as message (message.id)}
        {#if datesGroup.indexOf(formatDate(String(message.sentat), false).ofYear) !== -1}
            <div class="date-display">
                {formatDate(String(message.sentat), false).ofYear}
            </div>
        {/if}
        {/* Rest of the loop content */}
    {/each}
    
    1. Use a memoized function: Another approach is to use a memoized version of the formatDate() function, which will cache the results and return the same values for the same input. This can help ensure that the function returns the same results in the same order as the loop iteration.
    // Memoized formatDate function
    const memoizedFormatDate = memoize((dateStr) => formatDate(dateStr, false));
    
    {#each messages as message (message.id)}
        {#if memoizedFormatDate(String(message.sentat)).isNewDate}
            <div class="date-display">
                {memoizedFormatDate(String(message.sentat)).ofYear}
            </div>
        {/if}
        {/* Rest of the loop content */}
    {/each}
    
    function memoize(fn) {
        const cache = new Map();
        return function(...args) {
            const key = JSON.stringify(args);
            if (cache.has(key)) {
                return cache.get(key);
            } else {
                const result = fn.apply(this, args);
                cache.set(key, result);
                return result;
            }
        };
    }