0

I have this small React component, and it doesn't seem to be setting the state of my authToken in the useEffect() call.

Here is my code:

const App = ({ personId, buildingId }) => {
 
     const [authToken, setAuthToken] = useState();

     useEffect(() => {
             axios.post('/api/auth/' + personId + '/location/' + buildingId, {
                 headers: {
                     'Content-Type': 'application/json',
                 }
             }).then((res) => {
                 setAuthToken(res.data); 
             })
         }, []);
         

    return (
                 <Editor
                     init={{
                         tinydrive_token_provider: function (success, failure) {
                             success({ token: authToken.token });
                        }}
                    />
             )
         }

export default App;

Do I need to set it some other way?

Thanks!

SkyeBoniwell
  • 6,345
  • 12
  • 81
  • 185
  • 2
    What indicates it isn't being set? The code currently accesses `authToken` in the render before the XHR request has finished. – Dave Newton Oct 13 '22 at 15:24
  • 2
    I don't know what `Editor` does, but I suspect that the `init` prop's `tinydrive_token_provider` is called once, and it throws an error since `authToken` is `null`. Can you confirm this? – Yanick Rochon Oct 13 '22 at 15:25
  • @DaveNewton when I was debugging, I put a `console.log()` right after `then((res) => {` and I never saw it in the browser console... – SkyeBoniwell Oct 13 '22 at 15:27
  • @YanickRochon but shouldn't `useEffect()` run when I first load the component and set the stage of `authToken`? – SkyeBoniwell Oct 13 '22 at 15:27
  • 2
    @SkyeBoniwell Yes, but XHR requests are async and unlikely to finish before the initial render. – Dave Newton Oct 13 '22 at 15:28
  • 1
    @SkyeBoniwell If the `then` function isn't running there's a different issue. – Dave Newton Oct 13 '22 at 15:29
  • @DaveNewton oh I see, I did not know that. Is there a way to fix this or do I need to find another way? thanks – SkyeBoniwell Oct 13 '22 at 15:29
  • @SkyeBoniwell Fix what? Accessing `authToken` before it's been set? Sure, check to see if it has a value. – Dave Newton Oct 13 '22 at 15:29
  • @DaveNewton I meant maybe I shouldn't be using `useEffect` hook. Maybe I should just remove it from the `useEffect` and call it in some way when the component is first loaded? – SkyeBoniwell Oct 13 '22 at 15:32
  • Check https://stackoverflow.com/questions/56838392/how-to-call-an-async-function-inside-a-useeffect-in-react – niorad Oct 13 '22 at 15:39

1 Answers1

1

Try this:

const App = ({ personId, buildingId }) => {
  const handleTokenProvider = useCallback((success, failure) => {
    axios.post('/api/auth/' + personId + '/location/' + buildingId, {
      headers: {
        'Content-Type': 'application/json',
      }
    }).then((res) => {
      if (res && req.data && req.data.token) {
        success(res.data.token);
      } else {
        failure("Authentication failed");
      }
    }).catch(err => {
      failure(err);
    });
  }, [personId, buildingId]);

  return (
    <Editor
      init={{
        tinydrive_token_provider: handleTokenProvider
      }}
    />
  );
}

export default App;

The code above assumes a few things :

  1. That the property tinydrive_token_provider detects when it's value change and calls the function again when it does. If it does not, then there must be a way to update the token. And if it still does not, then unmounting the component and re-mounting it again will force recreating it.
  2. That you do not need the authentication token somewhere else in the component. If you do, then this solution will not be optimal. There would be ways to set a state (useState) or a reference (useRef) but there should be a way to call success/failure without requiring a component update.
  3. That every time you render the App component, a request will be made, even if you just got the token for the same props. Having a way to store this token in a cache (e.g. localStorage) could greatly improve performance on refresh.

Alternative

If you need to have access to the token elsewhere than the Editor, this would also work :

// this function get be in a separate module!
const fetchAuthToken = (personId, buildingId) => new Promise((resolve, reject) => {
  // TODO : check some cache first, and resolve if a
  //        previous token already exists and is not expired, etc.

  axios.post('/api/auth/' + personId + '/location/' + buildingId, {
    headers: {
      'Content-Type': 'application/json',
    }
  }).then((res) => {
    if (res?.data?.token) {
      resolve(res.data.token);
    } else {
      reject("Authentication failed");
    }
  }).catch(err => {
    reject(err);
  });
});


const App = ({ personId, buildingId }) => {

  const tokenPromise = useMemo(() => 
     fetchAuthToken(personId, buildingId),
     [personId, buildingId]
  );

  // DEBUG ONLY
  useEffect(() => {
    tokenPromise.then(token => {
       console.log('Token for', personId, buildingId, 'is', token);
    }, err => {
       console.error('Failed to get token for', personId, buildingId);
       console.error(err);
    });
  }, [tokenPromise, personId, buildingId]);

  return (
    <Editor
      init={{
        tinydrive_token_provider: (success, failure) => {
           tokenPromise.then(success, failure);
        }
      }}
    />
  );
}

export default App;
Yanick Rochon
  • 51,409
  • 25
  • 133
  • 214
  • Oh I see, so you took out useEffect and just made a new function that I can use to do check and set the value of `authToken`? I also see you took out the state management of `authToken`. So I don't need that or useEffect at all? thanks – SkyeBoniwell Oct 13 '22 at 15:46
  • I tried this and Chrome did not like the periods in `if (res?.data?.token) {` It said `Unexpected token` with a little carot pointing to the period in `res?.data?` – SkyeBoniwell Oct 13 '22 at 16:13
  • 1
    @SkyeBoniwell What version of Chrome? AFAIK this is currently supported in Chrome. – Dave Newton Oct 13 '22 at 16:41
  • @SkyeBoniwell if, for some reason, your browser does not like the optional chaining operator (`?.`), then the equivalent is : `if (req && req.data && req.data.token)`. (**Edit:** so... you are using React without transpiler??) – Yanick Rochon Oct 13 '22 at 16:45
  • @YanickRochon what is a transpiler? thanks! – SkyeBoniwell Oct 13 '22 at 19:05
  • 2
    @SkyeBoniwell In this case a transpiler turns the JS-you-write into JS-the-browser-understands, although Chrome has supported [optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) for a [pretty long time](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining#browser_compatibility). – Dave Newton Oct 13 '22 at 20:18