Server side rendered countdown custom component

Builder content link

Builder public api key
5e17fc51b0ef4dc3bfa9153bb7490049

What are you trying to accomplish
I want the ability to have my countdown rendered on the server so that the counter has the content and there isn’t a “flash” of content when the counter data loads. I have built a custom component and registered it with Builder. The sole purpose of the component is to calculate the time left and update the state every second.

I then have Text blocks that I use string interpolation to read the state for example <p>Memorial day sale ${state.timeLeft}</p>

The problem I am facing is that unless I set the state manually on the server by calling calculateTimeLeft Then I get a flash of “undefined” where the counter would be. I can set the state to an empty string but I still get the flash. I am wondering if there is another way to do this? I don’t mind calling the calculateTimeLeft on the server, however I can’t seem to find a way to access the date I input on the Builder.io visual builder.

<BuilderComponent
        model='announcement-bar'
        content={announcmentData}
        data={{
          hidden: isHiddenByCookie || isHiddenByAuthentication,
          timeLeft: calculateTimeLeft('May 22, 2024 19:00:00')
        }}
        context={{ setCookie }}
      />

Code stack you are integrating Builder with
React.js with Razzle and After.js

Reproducible code example

import React from 'react';
import { BuilderStoreContext } from '@builder.io/react';

export const calculateTimeLeft = (expiration) => {
	const difference = +new Date(expiration) - +new Date();
	let timeLeft = {};

	if (difference > 0) {
		timeLeft = {
			days: Math.floor(difference / (1000 * 60 * 60 * 24)),
			hours: Math.floor((difference / (1000 * 60 * 60)) % 24),
			minutes: Math.floor((difference / 1000 / 60) % 60),
			seconds: Math.floor((difference / 1000) % 60)
		};
	} else {
		return 'Expired';
	}

	const countdown = [];
	if (timeLeft.days > 0) {
		countdown.push(
			`${timeLeft.days} ${timeLeft.days > 1 ? 'days' : 'day'}`
		);
	}

	if (timeLeft.hours > 0) {
		countdown.push(
			`${timeLeft.hours} ${timeLeft.hours > 1 ? 'hours' : 'hour'}`
		);
	}

	if (timeLeft.minutes > 0) {
		countdown.push(
			`${timeLeft.minutes} ${timeLeft.minutes > 1 ? 'minutes' : 'minute'}`
		);
	}

	countdown.push(
		`${timeLeft.seconds} ${timeLeft.seconds > 1 ? 'seconds' : 'second'}`
	);

	return `Expires in ${countdown.splice(0, 2).join(' ')}`;
};

const Countdown = ({ expiration }) => {
	const builderContext = React.useContext(BuilderStoreContext);
	const isInitialized = React.useRef(false);
	if (builderContext.state.isServer && !isInitialized.current) {
		const initialTimeLeft = calculateTimeLeft(expiration);
		builderContext.update((state) => {
			state.timeLeft = initialTimeLeft;
		});
		isInitialized.current = true;
	}

	React.useEffect(() => {
		const timer = setInterval(() => {
			const newTimeLeft = calculateTimeLeft(expiration);
			builderContext.update((state) => {
				state.timeLeft = newTimeLeft;
			});
		}, 1000);

		return () => clearTimeout(timer);
	}, []);

	return null;
};

export default Countdown;
Builder.registerComponent(Countdown, {
  name: 'Countdown',
  inputs: [
    {
      name: 'expiration',
      type: 'date',
      defaultValue: 'December 30, 2024 03:24:00'
    }
  ]
});
1 Like

Hello @Yamaha32088,

If you’re using React.js with Razzle and After.js, you will need to use the custom server capabilities of Razzle in combination with After.js to fetch and pre-render your data.

Here’s how to integrate server-side data fetching and rendering for your countdown component using Razzle and After.js:

  1. Create Your Countdown Component: First, create your countdown component similar to the example above

    import React, { useState, useEffect } from 'react';
    
    const Countdown = ({ initialTimeLeft }) => {
      const [timeLeft, setTimeLeft] = useState(initialTimeLeft);
    
      useEffect(() => {
        const calculateTimeLeft = () => {
          const eventDate = new Date(); // Set this to the target date from Builder state
          eventDate.setSeconds(eventDate.getSeconds() + initialTimeLeft);
          const now = new Date();
          const timeLeftMillis = eventDate - now;
          const timeLeftSeconds = Math.max(Math.floor(timeLeftMillis / 1000), 0);
    
          setTimeLeft(timeLeftSeconds);
        };
    
        calculateTimeLeft(); // Update immediately in case it isn't calculated exactly right.
        const timerId = setInterval(calculateTimeLeft, 1000);
        return () => clearInterval(timerId);
      }, [initialTimeLeft]);
    
      return <p>Time left: {timeLeft} seconds</p>;
    };
    
    export default Countdown;
    
  2. Fetch Data on the Server: Use the custom server capabilities of Razzle to fetch data and pre-render your countdown component with the initial state.

    const express = require('express');
    const { builder } = require('@builder.io/react');
    const Countdown = require('./src/components/Countdown').default;
    
    builder.init('YOUR_API_KEY');
    
    const server = express();
    
    server
      .get('/countdown', async (req, res) => {
        // Fetch your target date from Builder
        const content = await builder.get('page', { url: '/your-builder-url' }).promise();
        const targetDate = new Date(content.data.targetDate);
        const now = new Date();
        const initialTimeLeft = Math.max(Math.floor((targetDate - now) / 1000), 0);
    
        res.render('countdown', { initialTimeLeft, content });
      })
      .listen(process.env.PORT || 3000);
    
  3. Update After.js Routes and Components: Modify your After.js routes to render the countdown component with the fetched data.

    import React from 'react';
    import { After } from '@jaredpalmer/after';
    import Countdown from './components/Countdown';
    import builder from '@builder.io/react';
    
    builder.init('YOUR_API_KEY');
    
    // Define routes
    const routes = [
      {
        path: '/',
        exact: true,
        component: async ({ match }) => {
          const content = await builder.get('page', { url: '/your-builder-url' }).promise();
          const targetDate = new Date(content.data.targetDate);
          const now = new Date();
          const initialTimeLeft = Math.max(Math.floor((targetDate - now) / 1000), 0);
    
          return (
            <>
              <Countdown initialTimeLeft={initialTimeLeft} />
            </>
          );
        }
      }
    ];
    
    export default routes;
    
  4. Server Rendering Configuration: Ensure your Razzle and After.js server configuration calls the correct routes and renders the content.

    import express from 'express';
    import { render } from 'razzle';
    import { After } from '@jaredpalmer/after';
    import routes from './src/routes';
    
    const server = express();
    
    server
      .get('/*', async (req, res) => {
        try {
          const html = await render({
            req,
            res,
            routes,
            document: () => <html />
          });
          res.send(html);
        } catch (error) {
          res.json(error);
        }
      });
    
    export default server;
    

This setup ensures that your countdown component is pre-rendered on the server with the initial state to avoid any flashes of “undefined.”

Note: Adjust the render() and other relevant configurations based on your project’s specific structure and requirements, as Razzle and After.js can be configured flexibly.

With this approach, you fetch the required data on the server, calculate the initial countdown time, and pass it as props to your component to ensure smooth, flash-free rendering.

Hi @manish-sharma I appreciate your response however this will not work and doesn’t answer the initial question. It appears to me that your response came from using ChatGPT or some other similar AI tool.

Hello @Yamaha32088,

We’re currently in the testing phase of an internal AI assistant tool that relies on our documentation and resources. I apologize if the response didn’t fully address your issue.

After thorough testing, we haven’t been able to reproduce the issue using the React SDK. However, we suspect that the problem might be related to your use of Razzle and After.js. To better assist you, could you provide us with a minimum reproducible repository?