revolunet / react-mailchimp-subscribe

React subscribe form for Mailchimp.
https://revolunet.github.io/react-mailchimp-subscribe/
244 stars 49 forks source link

[Proposal] Promise-based HOC wrapper for MailchimpSubscribe #70

Open smac89 opened 2 years ago

smac89 commented 2 years ago

I was recently tasked with implementing mailchimp subscription in react, but I found the implementation of MailchimpSubscrbe to be rather inconvenient for properly managing state.

The problem

In our case, we use react-google-recaptcha and react-final-form, but the problem is that final form expects that once we return from the onSubmit function, that the form has been submitted. Inside the onSubmit function, we wait for the recaptcha to complete, but when it comes to the subscribe callback exposed by MailchimpSubscribe, it does not return a promise; So calling that function inside the onSubmit function will immediately return which leads the form to assume the subscription was successful.

Solution

I came up with a higher-order component which simply exposes a function that returns a promise and you don't have to worry about status; Just wait for the function to complete and if there is an error at the end, you can "catch" that and do something with it. Here it is (hope you don't mind typescript :smile: ):

/* eslint-disable react-hooks/rules-of-hooks */
import React, { useCallback, useEffect, useRef } from "react";
import MailchimpSubscribe, { DefaultFormFields } from "react-mailchimp-subscribe";

interface MailchimpSubscribeHOCProps<MCF> {
    mailchimpSubscribe: (formFields: MCF) => Promise<void>;
}

/**
 * Wraps a component with the MailchimpSubscribe component and exposes a simple subscribe
 * function which returns a promise
 *
 * @param Wrapped The component to wrap
 * @param mailchimpSubscribeUrl The POST url used for subscribing via mailchimp
 * @returns A component which has the ability to subscribe to
 */
export default function withMailchimpSubscribe<T = {}, MCF = DefaultFormFields>(Wrapped: React.ComponentType<T & MailchimpHOCProps<MCF>>, mailchimpSubscribeUrl: string): React.FC<T> {
    const subscribeSuccess = useRef<() => void>(null);
    const subscribeError = useRef<(error?: any) => void>(null);

    useEffect(() => () => {
        // Avoid memory leaks(?) by removing any held references
        subscribeSuccess.current = null;
        subscribeError.current = null;
    }, []);

    // eslint-disable-next-line react-hooks/rules-of-hooks
    return useCallback((props: T) => (<MailchimpSubscribe<MCF>
        url={mailchimpSubscribeUrl}
        render={({ subscribe, status, message }) => {
            const mailchimpSubmit: (formFields: MCF) => Promise<void> = (formFields: MCF) => {
                subscribe(formFields);
                return new Promise<void>((resolve, reject) => {
                    subscribeSuccess.current = resolve;
                    subscribeError.current = reject;
                });
            };

            if (status === 'success' && subscribeSuccess.current != null) {
                subscribeSuccess.current();
            } else if (status === 'error' && subscribeError.current != null) {
                subscribeError.current(message);
            }
            return <Wrapped mailchimpSubscribe={mailchimpSubmit} {...props} />;
        }} />
        // eslint-disable-next-line react-hooks/exhaustive-deps
    ), [mailchimpSubscribeUrl]);
}

The promise resolves to nothing if the subscription was successful, otherwise it fails with an error message if the status is error.

Example

const mailchimpUrl = "...";
const MyCustomForm: React.FC = withMailchimpSubscribe(({ mailchimpSubscribe: subscribe }) => {
    // do some state management here

   return (
       <Form .../>
   );
}, mailchimpUrl);
revolunet commented 2 years ago

The current implementation is quite outdated indeed

What about a hook version like const { status, subscribe } = useMailchimpSubscribe(url); ?

smac89 commented 2 years ago

Oh yes a hook will be a much better design.