Home > database >  Using React Context in a custom hook always returns undefined
Using React Context in a custom hook always returns undefined

Time:12-28

I'm trying to create a custom hook which will eventually be packaged up on NPM and used internally on projects in the company I work for. The basic idea is that we want the package to expose a provider, which when mounted will make a request to the server that returns an array of permission strings that are then provided to the children components through context. We also want a function can which can be called within the provider which will take a string argument and return a boolean based on whether or not that string is present in the permissions array provided by context.

I was following along with this article but any time I call can from inside the provider, the context always comes back as undefined. Below is an extremely simplified version without functionality that I've been playing with to try to figure out what's going on:

useCan/src/index.js:

import React, { createContext, useContext, useEffect } from 'react';

type CanProviderProps = {children: React.ReactNode}

type Permissions = string[]

// Dummy data for fake API call
const mockPermissions: string[] = ["create", "click", "delete"]
  
const CanContext = createContext<Permissions | undefined>(undefined)

export const CanProvider = ({children}: CanProviderProps) => {
  let permissions: Permissions | undefined 

  useEffect(() => {
    permissions = mockPermissions
    // This log displays the expected values
    console.log("Mounted. Permissions: ", permissions)
  }, [])

  return <CanContext.Provider value={permissions}>{children}</CanContext.Provider>
}

export const can = (slug: string): boolean => {
  const context = useContext(CanContext)
  
  // This log always shows context as undefined
  console.log(context)

  // No functionality built to this yet. Just logging to see what's going on.
  return true
}

And then the simple React app where I'm testing it out:

useCan/example/src/App.tsx:

import React from 'react'

import { CanProvider, can } from 'use-can'

const App = () => {
  return (
    <CanProvider>
      <div>
        <h1>useCan Test</h1>
        {/* Again, this log always shows undefined */}
        {can("post")}
      </div>
    </CanProvider>
  )
}

export default App

Where am I going wrong here? This is my first time really using React context so I'm not sure where to pinpoint where the problem is. Any help would be appreciated. Thanks.

CodePudding user response:

You should use state to manage permissions. Look at the example below:

export const Provider: FC = ({ children }) => {
  const [permissions, setPermissions] = useState<string[]>([]);

  useEffect(() => {
    // You can fetch remotely
    // or do your async stuff here
    retrivePermissions()
      .then(setPermissions)
      .catch(console.error);
  }, []);

  return (
    <CanContext.Provider value={permissions}>{children}</CanContext.Provider>
  );
};

export const useCan = () => {
  const permissions = useContext(CanContext);

  const can = useCallback(
    (slug: string) => {
      return permissions.some((p) => p === slug);
    },
    [permissions]
  );

  return { can };
};

Using useState you force the component to update the values. You may want to read more here

CodePudding user response:

There are two problems with your implementation:

  1. In your CanProvider you're reassigning the value in permissions with =. This will not trigger an update in the Provider component. I suggest using useState instead of let and =.
const [permissions, setPermissions] = React.useState<Permissions | undefined>();

useEffect(() => {
  setPermissions(mockPermissions)
}, []);

This will make the Provider properly update when permissions change.

  1. You are calling a hook from a regular function (the can function calls useContext). This violates one of the main rules of Hooks. You can learn more about it here: https://reactjs.org/docs/hooks-rules.html#only-call-hooks-from-react-functions

I suggest creating a custom hook function that gives you the can function you need.

Something like this, for example

const useCan = () => {
  const context = useContext(CanContext)
  return () => {
    console.log(context)

    return true
  }
}

Then you should use your brand new hook in the root level (as per the rules of hooks) of some component that's inside your provider. For example, extracting a component for the content like so:

const Content = (): React.ReactElement => {
  const can = useCan();

  if(can("post")) {
    return <>Yes, you can</>
  }

  return null;
}

export default function App() {
  return (
    <CanProvider>
      <div>
        <h1>useCan Test</h1>
        <Content />
      </div>
    </CanProvider>
  )
}
  • Related