resend / react-email

💌 Build and send emails using React
https://react.email
MIT License
14.42k stars 656 forks source link

Getting Dynamic Content from API before Render #516

Closed Bugs5382 closed 9 months ago

Bugs5382 commented 1 year ago

So, as always, emails are static. However, some of the vars, and fields that I would want filled out are still static, but the information, like a URL, data, etc. could be dynamic from an API source, database, etc.

To better illustrate this feature, I have would have this custom component:

<KB number={"0010002"}/>

This would do a AXIOS call to:

https://MYINSTANCE.service-now.com/api/now/table/ (sans the rest the of API call)

... get the output of the response. Parse it and get the data I want to be able to do the render output in the email.

And on the "Desktop" view it would render:

"KB0010002" as a link to https://my.kbsystem.link/kb?=SYS_ID. My "KB" componey would use the <Link> of React Email for the final output.

On the "HTML" and "Plan Text" side it would still render our the URL as needed.

This way it would allow me to hardcode records inside the email render/template, but since the content is dynamic based off the version of the KB article in my example, it has to be maintained.

Ideas on how to do this? I also got some code working, however the biggest issue is that this system is designed to only work with static content and render it. This has to "pre-render" and then also "pre-output the text" before the render overall function of React Email works.

Thanks for your time.

bukinoshita commented 1 year ago

To understand a bit better, are you trying to render dynamic content from an API to the react email preview app?

Bugs5382 commented 1 year ago

Yeah. This morning when I woke up, I thought maybe use swr package, but still no go.

The idea is that the API daaa while it might be dyanmic and/or static in the DB, the emails output (source, preview, etc) would be still it's static self.

I came up with this:

export function KB (props: any)  {
    console.log(props)
   const {data, error} = useSWR('fire', servicenow)
    // const {data, error} = useSWR(    '1123', servicenow)
    console.log(data)
    // return <Link href="https://{full url}">{display name}</Link>
    return "TESTING"
};

But useSWR never fires.

bukinoshita commented 1 year ago

What's the use case to fetch db to preview in the email locally? I think you could use props and default props values, but when you're about to send the email, then you pass the dynamic values from the database to the email created.

Bugs5382 commented 1 year ago

The Issue

I think the biggest issue that I have seen is that fetch, axios, etc. are sync. I keep getting a promise issue when rendering.

Background

So my use case at least is that I am developing all these email notification for our ServiceNow implantation. #447 was the issue for that first part.

So ServiceNow, in a nut shell has their email notification system divided into two parts:

So I have now:

const EndNewIncident_Full = () => {

    return (
        <IncidentTemplate>
           <EndNewIncident_Body />
        </IncidentTemplate>
    )
}

As a sinle file in react-email and it does render out what I want.

I wrote another script inside my project that will take the output of <IncidentTemplate> and export the HTML and the Plain Text into the correct "database" fields in my ServiceNow Instance for the template. and <EndNewIncident_Body /> goes into the actual notification plain text and HTML fields. I linked these up with the generated GUID for those records since they are always the same. This allows my team to also in my Git repo, my team to go in and make these corrections to emails for spelling/grammer, and also additional information they want to include in the email notifications via internal PR requests.

Why Dynamic?

However, as part of these emails, while I can insert a "string" ${caller_id} and ServiceNow will replace it with the value that it will do based off the record it's generated from, the KB articles that I want to reference in the email notification, like polcies, etc. the links are ever changing based off the newest version that come from the KB system of ServiceNow. While I know if I make an update to the KB artcile and the link would be different, my script above I could fire at anytime to update the development instance and then I can just push it out to production without having to go into this tiny editing window which is a pain already.

So I've looked through and tried different types of component structure with no luck. So the closest I come is [Promoise waiting] result, however, as soon as I start doing async/await things stop working.

<KB number={"0010002"}/> would do an API call and get the most recent published version of the KB article URL and then store it in the email and display it using the <Link> element already there.

ServiceNow does not render code, it only has an output. If it had the ability to do it on the fly, yes, then I would be able to do it, but at this point, ServiceNow logic it doesn't.

Questions?

Let me know!

dreinon commented 10 months ago

Hi @bukinoshita! This would be really useful, I'll try to explain why from my perspective.

This project does not limit what kind of objects we can pass as email component props. For instance, in my project, I have classes for User and Event that are constructed from a firestore document resulting from an API call to firestore, which cannot be easily mocked. These classes provide many methods and calculated properties that can be used in the emails, better than only passing the data as a regular data object as props.

Imagine an email with a props object

interface Props {
    user: User;
    event: Event;
}

const Email = ({user, event}: Props) => {};

In order to pass a mock, what I would actually need is to make a function that calls firestore in order to get a document for user and a document for event, that I can use to construct to objects of User and Event classes, which will be my mocks.

const getMockData = async () => {
    const userDoc = await firestore().collection('user').doc('whatever').get();
    const eventDoc = await firestore().collection('event').doc('whatever').get();
    return {
        user: new User(userDoc),
        event: new Event(eventDoc)
    }
}

But then I find problems in both ways I can call my getMockData function:

  1. I cannot call the getMockData as a top-level await
    
    interface Props {
    user: User;
    event: Event;
    }

const mockData = await getMockData(); // ERROR top-level await

const Email = ({user, event}: Props = mockData) => {};


2. I cannot call getMockData in a parameter initializer:
```ts
interface Props {
    user: User;
    event: Event;
}

const Email = ({user, event}: Props = await getMockData()) => {}; // ERROR await cannot be used in a param initializer.

A way to be able to call getMockData would be what this issue is asking for.

Hope it clarifies and makes this feature be implemented sooner! Congrats for the project!