2

I have a component that displays data returned from a callback received in the props. I am hoping there is a way to force the use of memoized callbacks so a useEffect hook does not run infinitely due to accidental passing of anonymous functions.

const DataRenderer = (props: {asyncDataFn: () => Promise<object[]>}) => {
    const [data, setData] = useState([]);    

    useEffect(() => {
        props.asyncDataFn().then(r => setData(r)),
    [props.asyncDataFn]);

    ...
}


const ShowData = () => {
    // TypeScript should warn here and say that my callback isn't memoized
    return <DataRenderer asyncDataFn={() => return [{...}]}/>
}

Is there a way to make the asyncDataFn type something like:

asyncDataFn: Memoized<() => Promise<object[]>>
2
  • Why does DataRenderer receive a callback rather than the actual data? That would be simpler, and bypass the problem of the infinite loop. Commented Sep 13, 2021 at 12:39
  • I am planning on enhancing it later to automatically add progress bars and abort signals for fetching data from a remote api. Commented Sep 13, 2021 at 12:58

1 Answer 1

1

I don't think TypeScript has anything built in for this, not least because "memoized" is a relative term.

I would make ShowData responsible for getting the data, and have it give DataRenderer the actual data, not a callback to request it. Simpler, and it bypasses the problem with the infinite render loop.

If you don't want to do that and you want a type-based solution, you could brand functions you consider memoized. Here's the Memoized type and a hook to do the memoization:

type Memoized<T> = T & {__memoized__: true};

const useMemoized = <FunctionType extends () => unknown,>(fn: FunctionType, deps: DependencyList | undefined) => {
    const x = useMemo(fn, deps) as Memoized<FunctionType>;
    x.__memoized__ = true;
    return x;
};

Note that this doesn't ensure proper memoization (because that's context-sensitive), purely that the function has been asserted to be memoized.

Here's what DataRenderer would look like (with a couple of typos fixed as well):

const DataRenderer = (props: {asyncDataFn: Memoized<() => Promise<object[]>>}) => {
    const [data, setData] = useState<object[]>([]);    

    useEffect(() => {
        props.asyncDataFn().then(r => setData(r));
    }, [props.asyncDataFn]);

    // ...
};

Now this works:

const ShowData = () => {
    // Works
    const asyncDataFn = useMemoized(async () => { return [{/*...*/}]; }, []);
    return <DataRenderer asyncDataFn={asyncDataFn}/>
};

But this raises an error as desired:

const ShowData = () => {
    // Error as requested
    const asyncDataFn = async () => { return [{/*...*/}]; };
    return <DataRenderer asyncDataFn={asyncDataFn}/>
    //                   ^^^^^^^^^^^ Type '() => Promise<{}[]>' is not assignable
    //                               to type 'Memoized<() => Promise<object[]>>'.
    //                               Property '__memoized__' is missing in type
    //                               '() => Promise<{}[]>' but required in type
    //                               '{ __memoized__: true; }'.(2322)
};

Playground link

Again, though, I don't think I'd do it that way without some strong driving factor.

Sign up to request clarification or add additional context in comments.

2 Comments

Even though I am convinced now that my approach is wrong, I still love the idea. Until the strong driving factor is discovered (which I believe could be somewhere out there), here are some extra thoughts: I'll just swap out the memoized for a unique symbol to ensure no conflicts can occur with the attributes. const memoSymbol: unique symbol = Symbol(); type Memoized<T> = T & { [memoSymbol]: true };
If we want to be even more bonkers: declare module "react" { function useMemo<T>( factory: () => T, deps: DependencyList | undefined ): Memoized<T>; function useCallback<T extends (...args: any[]) => any>( callback: T, deps: DependencyList ): Memoized<T>; }

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.