Search code examples
node.jsreactjsapollonext-auth

useContext returning null upon useQuery


I am making a wrapper for the settings page in order for it to be reusable on every settings page. In it, there is a Context Provider in which I pass the user data, but it never seems to pass anything although the query is successful.

SettingsWrap.tsx

export const UserContext = React.createContext(null);

const SettingsWrap = ({ children }) => {
  const [session] = useSession();
  const { data, loading } = useQuery<SettingsData, SettingsVars>(
    SETTINGS_QUERY,
    {
      variables: {
        id: session?.id,
      },
      skip: !session,
    }
  );

  return (
    <Grid container spacing={2}>
      <Grid item xs={12} sm={4}>
        <SettingList />
      </Grid>
      {data && !loading ? (
        <UserContext.Provider value={data.userId}>
          <Grid
            item
            xs={12}
            sm={8}
            style={{ height: "100vh", overflow: "auto" }}
          >
            <Container>{children}</Container>
          </Grid>
        </UserContext.Provider>
      ) : (
        <Grid
          item
          xs={12}
          sm={8}
          style={{
            display: "flex",
            justifyContent: "center",
            alignItems: "center",
            height: "100vh",
          }}
        >
          <CircularProgress />
        </Grid>
      )}
    </Grid>
  );
};

AccountSettings.tsx

  const user: UserInterface = useContext(UserContext); // THIS IS NULL

What is wrong with this code?


Solution

  • A context has a default value and can then be set to different other values by the use of providers, much like you have done. The value of a provider affects only the components in the children subtree of the component. Conversely, the current value of a context is the value set by the closest ancestor provider or, if none exists, the default value of the context.

    Check the following example:

    export const NumberContext = React.createContext(0); // default value
    
    export default function App() {
      return (
        <div>
          <Consumer /> // #1, shows 0
          <NumberContext.Provider value={1}>
            <Consumer /> // #2, shows 1
            <NumberContext.Provider value={2}>
              <Consumer /> // #3, shows 2
            </NumberContext.Provider>
            <div>
              <div>
                <Consumer /> // #4, shows 1
              </div>
            </div>
          </NumberContext.Provider>
        </div>
      );
    }
    
    export function Consumer() {
      const number = useContext(NumberContext)
    
      return <p>{number}</p>
    }
    

    Here is a sandbox with this example: https://codesandbox.io/s/react-contexts-6dlh1

    • The first Consumer (#1) has no ancestor provider so it shows the default value of the context created which is 0.
    • The second Consumer (#2) is the direct child of a provider with value 1, and so shows value 1.
    • The third Consumer (#3) has two ancestor providers but the closest ancestor provider provides the value 2, so that's the value this component shows.
    • The fourth and last Consumer (#4) is nested under the first provider (since it lies outside the second provider but still inside the children hierarchy of the first provider). Thus it has an ancestor provider with the value 1 so it will also show value 1.

    In your example, SettingsList (parent of AccountSettings), which is the presumed consumer of UserContext, does not have an ancestor provider (since it does not lie under the provider you have included in the component tree). Thus running useContext(UserContext) will always return null, the default value or UserContext, in that subtree of your component. Try putting SettingsList in a subtree with an ancestor provider and it will work.