I have a component that renders relative time
import React, { useEffect, useRef, useReducer } from 'react'
import { Text, TextProps } from 'react-native';
import { formatDistanceToNow } from 'date-fns';
type MomentTextProps = Omit<TextProps, 'children'> & {
date: Date | number;
pollTimeMs?: number;
}
export function MomentText({
date,
pollTimeMs = 60000,
...props }: MomentTextProps): JSX.Element {
const [formatted, update] = useReducer(
() => formatDistanceToNow(Date.now()),
formatDistanceToNow(Date.now())
);
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
function doUpdate() {
update();
timeoutRef.current = setTimeout(update, pollTimeMs);
}
useEffect(() => {
doUpdate();
return () => {
clearTimeout(timeoutRef.current!);
};
});
return <Text {...props}>{formatted}</Text>
}
Which works, but seems overkill that I am creating so many timeouts, I was wondering if there's a way of doing this via a "React context" but only rerender this one specific child.
The way I am thinking of it (as I type this) is to have a subscribe/notify hook like this...
/**
* This hook provides a simple subscription semantic to React components.
*/
export function useSubscription<T = unknown>(): SubscriptionManager<T> {
const subscribersRef = useRef<((data: T) => void)[]>([]);
function subscribe(fn: (data: T) => void) {
subscribersRef.current.push(fn);
return () => {
subscribersRef.current = subscribersRef.current.filter(
(subscription) => !Object.is(subscription, fn)
);
};
}
function notify(data: T) {
subscribersRef.current.forEach((fn) => fn(data));
}
function useSubscribeEffect(fn: (data: T) => void) {
useEffect(() => subscribe(fn), []);
}
return { subscribe, notify, useSubscribeEffect };
}
and have a useRef and useEffect that will call notify on the MomentText component.
Seems quite overkill but not really sure which is worse.
The first approach is easy to understand, but may be a performance hit
The second is a bit more complicated, but the usage is isolated since it's just a context requirement and everything else can be handled by subscriptions within the component.
Seems like the idea I had with the context would indeed work, but I had to create a new hook called useClock which is implemented below.
import { useEffect, useRef } from "react";
import { SubscriptionManager, useSubscription } from "../useSubscription";
/**
* time to next full minute from the date
* @param date
* @returns milliseconds to next full minute from date
*/
function timeToNextFullMinute(date: number): number {
return 60000 - (date % 60000);
}
/**
* This hook notifies the subscribers with the current date. This
* is useful for updating components that show moment in time without
* a full rerender of the tree.
*
* It notifies at the following points:
* - useEffect
* - first minute at zero seconds
* - every minute after at zero seconds
*
* However, due to the performance of the device, this may not be acurate.
*
* @returns subscription manager
*/
export function useClock(): SubscriptionManager<number> {
const { subscribe, notify, useSubscribeEffect } = useSubscription<number>();
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
function doNotify() {
notify(Date.now());
timeoutRef.current = setTimeout(doNotify, timeToNextFullMinute(Date.now()));
}
useEffect(() => {
doNotify();
return () => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
clearTimeout(timeoutRef.current!);
};
}, []);
return { subscribe, notify, useSubscribeEffect };
}
My context would then provide the useSubscribeEffect specific to the useClock hook which I then use from MomentText which would be:
type MomentTextProps = Omit<TextProps, "children"> & {
on: Date | number;
};
export function MomentText({ on, ...props }: MomentTextProps): JSX.Element {
const { useClockSubscribeEffect } = useDevhausUI();
const { formatRelativeToNow } = useLocalization();
const [text, setText] = useState(formatRelativeToNow(on));
useClockSubscribeEffect(() => setText(formatRelativeToNow(on)));
return <Text {...props}>{text}</Text>;
}
With this approach, the timeout is managed in the useClock() hook and it is part of a context so there's not too many running around.
Source and test available at https://github.com/trajano/react-hooks-tests/tree/master/src/useClock