focus-trap / focus-trap-react

A React component that traps focus

Home Page:http://focus-trap.github.io/focus-trap-react/demo/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Callback props don't seem to update with state changes

AlexKDawson opened this issue Β· comments

Repro: https://codesandbox.io/s/focus-trap-react-issue-forked-jgei7y?file=/src/App.js

Hello! Thanks for maintaining this library, I just discovered it recently and it's been a huge help. I have however run into this one issue, I believe its a bug but may just be something I'm doing wrong.

In short, when I pass callbacks into the onDeactivate/onPostDeactivate/clickOutsideDeactivates if those callbacks use parent state, the callbacks seem to never be updated past the first render. In general it seems like no updates to the focusTrapOptions trigger a rerender of the FocusTrap at all after initial render. (with the exception of the call to setActive in my example above) but I suspect this is due to active being passed in as its own prop.

For the repo above:

Expected behavior:
num is initialized to 0
The app renders the number 100 after the useEffect is triggered.
Activating then deactivating the Focus Trap should cause the number 100 to be logged as the most recent value of num.

Actual behavior:
num is initialized to 0
The app renders the number 100 after the useEffect is triggered.
Activating then deactivating the Focus Trap logs 0 to the console.

@AlexKDawson I'm glad you're finding the library useful! πŸ˜„

I can't say I'm surprised by this now that you've brought up the issue related to React hooks. focus-trap-react was created (before React hooks were introduced in React v16.8) as a React wrapper for focus-trap which has nothing to do with React -- especially not functional React using hooks where you might pass an event handler tied to state which gets created on every render or created whenever that state changes (if you useCallback() or useMemo() to generate the handler).

All those handlers are properties of the focusTrapOptions prop, and those options get passed into focus-trap internally. And once the trap is created, the focusTrapOptions is completely ignored because there's no way to update the created trap with a new set of options. That would require a big update to focus-trap itself to make it possible.

So it's really a one-time thing once the trap has been created. And looking at the code, the focusTrapOptions prop can really only be provided once on first render. It's never looked at again. One of the main reasons for that (and I'm just speaking to how the code was/is after I became the maintainer a few years ago) is that the <FocusTrap> component creates its own internal set of focus-trap options and then iterates the focusTrapOptions object's properties and picks it apart. So even the (potentially) shared object reference is lost.

I believe this works fine with classical React (i.e. class-based where you use a JS class to define a component), however, because the component's state is a class member, and provided you pass handlers bound to the class instance to focusTrapOptions, you'll always have access to the latest state via this.state... in your handlers. You can see how this works in the main code example.

So my advice here would be to either switch to a class for the component that has state and which uses <FocusTrap> (which, I understand, might be a "fly in the ointment" in a code base that uses functional components πŸ˜‰ ), or -- but this probably makes things a lot harder to manage -- useRef({ foo: 1, bar: 2 }) for the bits of state that need to be accessed by the handlers you're giving to the trap.

If you concur with this, then I'll add a warning about this in the focusTrapOptions prop docs for posterity.

Thanks again for being so responsive and explaining this.

That all makes sense to me, I think a warning in the docs would be beneficial!

I thought some on the two approaches you suggested and I think I'm going to go with the latter albeit potentially more risky approach for my specific use case. I think using the example below I can combine the useRef with the existing useState and a useEffect hook to make a copy of the state object on each render inside the ref. As long as I only pass the ref values into any focusTrap callbacks, there shouldn't be any additional difficulty.

That said, I could see if the state starts to get a bit more complex than one or two read only variables this approach could get out hand quickly πŸ™‚ But I'll keep that in mind!

https://codesandbox.io/s/focus-trap-react-issue-forked-oj47v1?file=/src/App.js

Thanks again for being so responsive and explaining this.

You're welcome!

That all makes sense to me, I think a warning in the docs would be beneficial!

Warning has been added!

I thought some on the two approaches you suggested and I think I'm going to go with the latter albeit potentially more risky approach for my specific use case. I think using the example below I can combine the useRef with the existing useState and a useEffect hook to make a copy of the state object on each render inside the ref. As long as I only pass the ref values into any focusTrap callbacks, there shouldn't be any additional difficulty.

That said, I could see if the state starts to get a bit more complex than one or two read only variables this approach could get out hand quickly πŸ™‚ But I'll keep that in mind!

https://codesandbox.io/s/focus-trap-react-issue-forked-oj47v1?file=/src/App.js

Sounds good, and thanks for sharing the sandbox for posterity!

@stefcameron

I've put up a pull request to add @AlexKDawson! πŸŽ‰