-
-
Notifications
You must be signed in to change notification settings - Fork 106
feat: synchronous handler mount #538
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
📊 Package size report
|
|
The latest updates on your projects. Learn more about Argos notifications ↗︎
|
80a9d94
to
a922738
Compare
This was referenced Aug 9, 2024
Merged
kirillzyusko
added a commit
that referenced
this pull request
Aug 9, 2024
…n` peer dependencies (#542) ## 📜 Description Minimal supported version of `react-native-reanimated` is `3.0.0`. ## 💡 Motivation and Context These changes were introduced in #538 In this PR I'm just changing version in `package.josn`. Follow up for #538 ## 📢 Changelog <!-- High level overview of important changes --> <!-- For example: fixed status bar manipulation; added new types declarations; --> <!-- If your changes don't affect one of platform/language below - then remove this platform/language --> ### JS - bump `react-native-reanimated` to `3.0.0`; ## 🤔 How Has This Been Tested? There is no way to test it 😀 ## 📝 Checklist - [x] CI successfully passed - [x] I added new mocks and corresponding unit-tests if library API was changed
Closed
2 tasks
kirillzyusko
added a commit
that referenced
this pull request
Sep 5, 2024
) ## 📜 Description On fabric with `StrictMode` enabled worklet handlers were not working. It was because of the fact that effects are fired twice, and current implementation of `useSyncEffect` simply added handler and unmounted it when effect is fired twice intentionally. ## 💡 Motivation and Context Original PR that introduced these changes was #538 Theoretically this problem could be fixed by using a version like: ```tsx import { useEffect, useRef } from "react"; import type { DependencyList } from "react"; /** * @description * Equivalent to `useEffect` but will run the effect synchronously, i. e. before render. * * @param {effect} - imperative function * @param {deps} - if present, effect will only activate if the values in the list change * * @author Kiryl Ziusko * @SInCE 1.13.0 * @Version 1.0.0 */ const useSyncEffect: typeof useEffect = (effect, deps) => { const cachedDeps = useRef<DependencyList | undefined | null>(null); const areDepsEqual = deps?.every( (el, index) => cachedDeps.current && el === cachedDeps.current[index], ); const cleanupRef = useRef<(() => void) | void>(); if (!areDepsEqual || !cachedDeps.current) { cleanupRef.current?.(); cleanupRef.current = effect(); cachedDeps.current = deps; } useEffect(() => { // strict mode double effect invocation handling if (deps !== cachedDeps.current) { cleanupRef.current = effect(); cachedDeps.current = deps; } }, deps); useEffect(() => { return () => { cleanupRef.current?.(); cachedDeps.current = null; }; }, []); }; export default useSyncEffect; ``` But in this case: - effects are not running synchronously (second run from react in `StrictMode` will run it asynchronously anyway because it'll be triggered from `useEffect`); - we mutate ref during render which is also not desirable in concurrent react; - the `StrictMode` fix can not be covered by unit tests, which indicated that we go too deeply and probably doing something wrong if it can not be covered by unit tests. So keeping that in my head I decided to make a step back and rewrite this part - now instead of `useSyncEffect` I will use `useLayoutEffect`. In this case I'll still run effects slightly earlier than plain `useEffect` and it should be alright for now. I've tested Expensify app with these changes and now I don't see advantages of using `useSyncEffect` over `useLayoutEffect` - both of them give roughly speaking the same result. I also thought about using `useInsertionEffect` (I'm not css-in-js library developer, but still) - but in this case I also didn't discover any advantages and react warns that it's better to stick to default `useEffect`/`useLayoutEffect` hooks. Potentially we can use `useInsertionEffect` but it should be a last resort and for now I prefer to use `useSyncEffect` 😅 > [!IMPORTANT] > Even though we migrated to default lifecycles it still worth to note, that fix #575 is still relevant, as `ref` may still not be updated by the time when effect is called 🤷♂️ ## 📢 Changelog <!-- High level overview of important changes --> <!-- For example: fixed status bar manipulation; added new types declarations; --> <!-- If your changes don't affect one of platform/language below - then remove this platform/language --> ### JS - remove `useSynceEffect` hook; - use `useLayoutEffect` instead of `useEffect` hook. ## 🤔 How Has This Been Tested? Tested on iPhone 15 Pro, iOS 17.5 (FabricExample app in `StrictMode` and Expensify app). ## 📝 Checklist - [x] CI successfully passed - [x] I added new mocks and corresponding unit-tests if library API was changed
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Labels
🚀 optimization
You optimize something and it becomes working faster
refactor
You changed the code but it didn't affect functionality
tests
You added or changed unit tests
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
📜 Description
Mount workletized handlers synchronously 😎
💡 Motivation and Context
Previously we were storing all worklet handlers in a global object and broadcasted events through all of them. However such approach had one big downside: updating shared value from JS is asynchronous (and if JS thread becomes busy, then mounting can take significant time).
In most of the cases it's not a big problem, but if keyboard moves when handler is not attached yet, then such keyboard movement is not getting tracked and as a result an actual keyboard position is not synchronized with shared values (if we are using components, such as
KeyboardAvoidingView
then they will not handle keyboard appearance properly).I've considered two approaches how to fix it:
1️⃣ Distinguish which events were not sent to particular handler and send them after actual mount
That was the first idea and I thought it's quite perspective, but when I implemented it, I quickly realized, that:
So at this point of time I realized, that it's not straightforward appraoch and started to look into different solutions.
2️⃣ Attach handler through JSI function
Another approach that I considered is attaching worklet handlers to the view directly (without intermediate handlers).
I discovered, that we call JSI function, which means that we have no delays or async stuff and was very inspired by that fact. I did some experiments and indeed it proved, that handlers can be attached synchronously.
However I discovered one use case - we still attach handler from
useEffect
(which is executed asynchronously) and worklet handlers are added viaaddUiBlock
, so it's again asynchronous. So even with JSI we could have a delay up to 2 frames, which wasn't acceptable in certain cases.At this point of time I thought that it's unreal to complete my objective, but then decided to try to use
useSyncEffect
. And withuseSyncEffect
it looks like we have onlyaddUIBlock
asynchronous and it's kind of acceptable (my handlers gets mounted faster, than keyboard events arrive).So I re-worked code, added unit tests for
useSyncEffect
, run e2e and CI and everything seems to be pretty good so fat.I know, that it's not very highly desirable to run synchronous events in react, but I feel like this approach is a last resort and want to try it. I also did performance benchmarks and didn't notice anything - UI gives 59/60 FPS and JS thread give 55 FPS (when navigating between screens). So I think this approach is an acceptable option that worth to try 🚀
📢 Changelog
Docs
react-native-reanimated@3.0.0
is a minimal supported version;JS
src
folder;useSharedHandlers
hook;useEventHandlerRegistration
hook;setKeyboardHandler
andsetInputHandlers
method;ref
toKeyboardControllerView
;event-mapping
andevent-handler
files;useSyncEffect
hook;useFocusedInputTextHandler
,useFocusedInputSelectionHandler
anduuid
functions;🤔 How Has This Been Tested?
Tested manually on iPhone 15 Pro.
Also verified that e2e tests are not failing.
📸 Screenshots (if appropriate):
354689909-b7d1ff9f-958b-4c17-ba79-7b829bee2fd5.mp4
Screen.Recording.2024-08-08.at.18.02.53.mov
📝 Checklist