Search code examples
reactjstypescriptstatewrapperchildren

Type props for a context provider wrapping <App/>?


I am defining a context provider to wrap my <App/> component like this:

// index.ts
ReactDOM.render(
  <ApolloProvider client={client}>
    <React.StrictMode>
      <AuthProvider>
        <App />
      </AuthProvider>
    </React.StrictMode>
  </ApolloProvider>,
  document.getElementById('root')
);

I got the context provider definiton working by passing {children} deconstructed from props with the React.ReactNode type, and the return wraps the children between two <AuthContext.Provider> tags like this:

// src/context/AutContext.ts

// Context definition
const AuthContext = React.createContext<{currentUser: User} | undefined>(undefined);

// Context provider wrapper definition:
const AuthProvider = ({ children }: { children: React.ReactNode }) => {
    const { currentUser } = useCurrentUse();
  
    return (
      <AuthContext.Provider value={{ currentUser }}>
        {children} // <- note the different ways of passing children to the Provider
      </AuthContext.Provider>
    );  
  };
    

But I am trying to streamline the code by reducing <AuthContext.Provider/> to a one liner, which right now I have working passing props regularly, but with type any:

// Context provider wrapper definition (one liner variant):
const AuthProvider = (props: any) => { // <- any type. props instead of destructured {children}
  const { currentUser } = useCurrentUser();

  return <AuthContext.Provider value={{ currentUser }} {...props} />; // <- spread one liner
};

Any idea of how should I type props in the second variant? I have tried inferring it but the result does not work (and is a mess), and using React.ReactNode as a type only works with {children}, not with the entire props.

In @types/react I have found a ProviderProps<T> which might be the solution, but if it is, I still don't know what to replace T with.


Solution

  • Looking at the type definition for ProviderProps<T> the generic T is used to type the value property.

    This means you would provide the same T as provided to React.createContext<T>().

    Simple Method:

    // We know from inspecting the ProviderProps type that it has two properties
    // The first property value is not to be passed in.
    // The second property children is an optional set of react nodes.
    // The React.FunctionComponent type with no generic added only has a prop of type 
    // children?: React.ReactNode.
    // This will make the props of AuthProvider compatible with the props of AuthContext.Provider.
    const AuthProvider: React.FunctionComponent = (props) => {
        const { currentUser } = useCurrentUser();
      
        return (
          <AuthContext.Provider value={{ currentUser }} {...props} />
        );
    };
    

    More Complex Method:

    type ContextValue = {currentUser: User} | undefined;
    
    // depending on your version of typescript Omit might not like types with generics
    // the work around for this is to create a wrapping type around Omit to trick it into
    // thinking that it is getting a type it approves of.
    // If this does not effect your version of typescript just use:
    // Omit<ProviderProps<ContextValue>, 'value'>
    type OmitValue<T> = Omit<T, 'value'>
    
    const AuthContext = React.createContext<ContextValue>(undefined);
    
    // We do not control the type ProviderProps, this may change in the future.
    // To give better future proofing we can use the ProviderProp type and remove the
    // properties we do not want to be changed (value in this case) using Omit.
    // This means that any additional properties added to ProviderProps in the future
    // will be valid for AuthProvider, but at this point in time it will just be
    // children.
    const AuthProvider: React.FunctionComponent<OmitValue<ProviderProps<ContextValue>>> = (props) => {
        const { currentUser } = useCurrentUser();
      
        return (
          <AuthContext.Provider value={{ currentUser }} {...props} />
        );
    };
    

    Update

    React 18 removes the default children property off of React.FunctionComponent or its proxy React.FC. To make the 'Simple Method' above work without type errors the new React.PropsWithChildren can be used.

    Simple Method:

    // We know from inspecting the ProviderProps type that it has two properties
    // The first property value is not to be passed in.
    // The second property children is an optional set of react nodes.
    // The React.FunctionComponent type with no generic added only has a prop of type 
    // children?: React.ReactNode.
    // This will make the props of AuthProvider compatible with the props of AuthContext.Provider.
    const AuthProvider: React.FunctionComponent<React.PropsWithChildren<{}>> = (props) => {
        const { currentUser } = useCurrentUser();
      
        return (
          <AuthContext.Provider value={{ currentUser }} {...props} />
        );
    };