reactjstypescriptreact-context

React useContext problem with Typescript -- any alternative to <Partial>?


I have a React app that uses useContext, and I'm having trouble getting the typing right with my context. Here's what I have:

import React, { useState, createContext } from 'react';
import endpoints from '../components/endpoints/endpoints';

interface contextTypes {
    endpointQuery: string,
    setEndpointQuery: React.Dispatch<React.SetStateAction<string>>,
    searchInput: string,
    setSearchInput: React.Dispatch<React.SetStateAction<string>>,
    filmSearch: string | undefined,
    setFilmSearch: React.Dispatch<React.SetStateAction<string>>
    pageIndex: number,
    setPageIndex: React.Dispatch<React.SetStateAction<number>>,
    resetState: () => void;
}

export const DisplayContext = createContext<Partial<contextTypes>>({});

interface Props {
    children: React.ReactNode;
}

const DisplayContextProvider = (props: Props) => {
    const { nowShowing } = endpoints;
    const [ endpointQuery, setEndpointQuery ] = useState(nowShowing);
    const [ searchInput, setSearchInput ] = useState('');
    const [ filmSearch, setFilmSearch ] = useState('');
    const [ pageIndex, setPageIndex ] = useState(1);

    const resetState = () => {
        setSearchInput('');
        setFilmSearch('');
        setPageIndex(1);
    };

    const values = {
        endpointQuery,
        setEndpointQuery,
        pageIndex,
        setPageIndex,
        filmSearch,
        setFilmSearch,
        searchInput,
        setSearchInput,
        resetState
    };
    
    return (
        <DisplayContext.Provider value={values}>
            {props.children}
        </DisplayContext.Provider>
    );
};

export default DisplayContextProvider;

The problem is, when I use <Partial<contextTypes>>, I get this error all over my app:

Cannot invoke an object which is possibly 'undefined'

Is there a way to fix this so I don't have to go around adding ! marks to everything where I get the undefined error? (I'm also pretty new to Typescript, so it's totally possible that I'm going about typing my context in completely the wrong way)


Solution

  • I think the issue is that you can't initialize the context with a useful default value, but you expect that the context provider will always be higher in the component tree.

    When I'm in this situation, I want the following behavior:

    So, I usually create a hook that wraps useContext and does the null check for me.

    import React, { useContext, createContext } from 'react';
    
    interface contextTypes {
        // ...
    }
    
    // private to this file
    const DisplayContext = createContext<contextTypes | null>(null);
    
    // Used by any component that needs the value, it returns a non-nullable contextTypes
    export function useDisplay() {
      const display = useContext(DisplayContext);
      if (display == null) {
        throw Error("useDisplay requires DisplayProvider to be used higher in the component tree");
      }
      return display;
    }
    
    // Used to set the value. The cast is so the caller cannot set it to null,
    // because I don't expect them ever to do that.
    export const DisplayProvider: React.Provider<contextTypes> = DisplayContext.Provider as any;
    

    If useDisplay is used in a component without a DisplayProvider higher in the component tree, it will throw and the component won't mount.