next-sanity
next-sanity copied to clipboard
Feature suggestion: update-events-only subscription hook
Since calls to Next.js' getStaticProps
and getServerSideProps
must often do further processing on fetched data, it's potentially a false assumption that the user actually wants the data to come through the subscription: rather, it may be more useful to listen for the update event, and then re-trigger the feeding of data from getStaticProps
and/or getServerSideProps
. A classic example of this is when the CMS data contains raw MDX code, and this needs to be compiled.
It turns out this can be done my using the Next router, and simply calling router.replace(router.asPath)
, which will cause all those prop-getting functions to re-run.
Since I just ran in to this issue with datocms' implementation of the same functionality, I offer this simplified hook based on their internal datocms-listen
library, which is capable of returning the data from the query but for which I just use it to get the events:
import { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import { subscribeToQuery } from 'datocms-listen';
export function useDatoCmsUpdate({
query,
variables = {},
preview = false,
updateRef = false, // pass data from gSP in here, to track `isUpdating` state
} = {}) {
const [error, setError] = useState(null);
const [status, setStatus] = useState(query ? 'connecting' : 'closed');
const [isUpdating, setIsUpdating] = useState(false);
const router = useRouter();
useEffect(() => {
if (!updateRef) return;
setIsUpdating(false);
}, [updateRef]);
useEffect(() => {
if (!query) return () => false; // return early if there's no query, preview mode is inactive
let unsubscribe;
async function subscribe() {
unsubscribe = await subscribeToQuery({
query,
variables,
preview,
token: process.env.DATOCMS_TOKEN,
onStatusChange: (s) => {
setStatus(s);
},
onChannelError: (e) => {
setError(e);
},
onUpdate: (updateData) => {
setError(null);
if (status !== 'closed') {
// hat tip: https://www.joshwcomeau.com/nextjs/refreshing-server-side-props/
router.replace(router.asPath);
if (updateRef) setIsUpdating(true);
}
},
});
}
subscribe();
return () => {
unsubscribe && unsubscribe();
};
}, []);
return { error, status, isUpdating };
}
This is a great idea, thanks for shedding light on the router.replace(router.asPath)
trick! I've asked the next.js team for clarification on the "stability" of this just to avoid implementing something that might break/change in the near future.
@rexxars your instinct about the stability was right, and I spoke too soon: there seems to be a bug that prevents it from working in production builds right now... 🤷🏻 https://github.com/vercel/next.js/issues/11559#issuecomment-609956467
While waiting for Next.js to fix that bug, the following works too, though less elegantly:
// top of page component
let allowReload = false;
useEffect(() => {
setTimeout(() => (allowReload = true), 5000);
}, []);
// later on...
if (allowReload) window.location.reload(true);
@rexxars the router.replace
trick works again, in my testing, latest as of Next.js v10.2.x
.
This is a part of my current solution with DatoCMS—I'm guessing it would be similarly applicable in Sanity, the question just being how to get the update event—if you've got further ideas I'd love to hear them ;)
import { useEffect, useRef, useState } from 'react';
import { subscribeToQuery } from 'datocms-listen';
import { useRouter } from 'next/router';
import { reporter } from '../utils';
/*
references for this technique of using router.replace
- https://www.joshwcomeau.com/nextjs/refreshing-server-side-props/
- https://twitter.com/flybayer/status/1333081016995622914
- https://medium.com/wearewebera/updating-the-properties-of-the-getstaticprops-method-in-next-js-79a17b7801e1
questions
- how can I return something useful from this, so I know if we are in a "loading" state?
- how can I prevent an additional run when the page first mounts?
*/
export function useDatoListeners(...queries) {
const router = useRouter();
const unsubsRef = useRef(queries);
const [isUpdating, setIsUpdating] = useState(false);
// const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const { info, table } = reporter();
if (router.isPreview) {
// setTimeout(() => setIsLoading(false), 1000);
unsubsRef.current = unsubsRef.current.map((query) =>
subscribeToQuery({
...query,
// preview: router.isPreview,
// token: process.env.DATOCMS_TOKEN,
onChannelError({ code, message, response }) {
table({ code, message, response });
window.location.reload(true); // retry the connection !
},
onUpdate() {
if (isUpdating) return;
info('datocms update received');
/* NOTES:
1. `shallow: false` means getStaticProps getServerProps will run
2. `scroll: false` maintains position, avoiding a scroll back to top
*/
router.replace(router.asPath, router.asPath, { shallow: false, scroll: false });
setTimeout(() => setIsUpdating(false), 1000);
},
}),
);
return () => Promise.all(unsubsRef.current).then((unsubs) => unsubs.forEach((unsub) => unsub()));
}
}, []);
return isUpdating;
}
In v2.0.0
you're encouraged in the docs to implement patterns where you're not data fetching on the server when you're in preview mode. As you still have to fetch in @sanity/groq-store
so it doesn't make sense to dual fetch 😄