Skip to content

Adds useTimestamp hook#9162

Open
ashfurrow wants to merge 3 commits intosoftware-mansion:mainfrom
ashfurrow:useTimestamp
Open

Adds useTimestamp hook#9162
ashfurrow wants to merge 3 commits intosoftware-mansion:mainfrom
ashfurrow:useTimestamp

Conversation

@ashfurrow
Copy link
Copy Markdown

@ashfurrow ashfurrow commented Mar 23, 2026

Summary

When driving animations or shaders based on the current time, I had to use this kind of code locally. It adds a bit of complexity to a component that already had a lot of complexity, and it was suggested to me that others could benefit from this.

The code accepts an isActive parameter which you could change based on your react-navigation focus state, for example. This pauses the animations while they're off screen.

Test plan

Equivalent code is working in production for me (I didn't patch the library, but I'm using the same calls). I also added thorough unit tests.

@tomekzaw
Copy link
Copy Markdown
Member

Thanks for the PR!

@tomekzaw
Copy link
Copy Markdown
Member

Actually there's one concern – if someone uses useTimestamp inside a component that has multiple instances, we will register multiple frame callbacks. It would be better if there would be at most one shared value (or mutable value) that is shared between all instances of this hook.


const component = render(<TestComponent />);
const view = component.getByTestId('AnimatedView');
expect(view.props.style[0].opacity).toBe(0);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you use the renderHook function to test the useTimestamp hook? It will be cleaner and simpler than the use of helper components. For example, here, you could simply do:

test('initializes to 0', () => {
  const { result } = renderHook(() => useTimestamp(false));
  expect(result.current.value).toBe(0);
});

Comment thread packages/react-native-reanimated/src/hook/useTimestamp.ts
@ashfurrow
Copy link
Copy Markdown
Author

ashfurrow commented Mar 23, 2026

It would be better if there would be at most one shared value (or mutable value) that is shared between all instances of this hook.

@tomekzaw I'm curious how you'd want to solve this? If we share frame callbacks across component instances, then we would not be able to share their active states. We would need some kind of update filtering. How lightweight would we want to keep this?

@tjzel
Copy link
Copy Markdown
Collaborator

tjzel commented Mar 24, 2026

@tomekzaw I'm curious how you'd want to solve this? If we share frame callbacks across component instances, then we would not be able to share their active states. We would need some kind of update filtering. How lightweight would we want to keep this?

I think ideally with a lazy initialization:

the file in top-level could describe e.g. let timestampSv = null and initialize it the first time the hook is called and then return it. It should be disabled once it has no listeners and enabled when it has at least one

@tomekzaw
Copy link
Copy Markdown
Member

tomekzaw commented Mar 24, 2026

@ashfurrow @tjzel Yes, exactly. There should be one global mutable value. useTimestamp hook should always return the reference to the same mutable value. Developers should be able to use useTimestamp within components that have multiple (~100) mounted instances. In such case, there should be one mutable value with 100 listeners (e.g. useAnimatedStyle calls).

Side-note: A better version of this would be that developers call useTimestamp hook once and pass it to components using props or React context. Alternatively, Reanimated could expose ReanimatedTimestampContext and useTimestamp would just read the shared value from this context. Personally I like the option without React context better.

We enable the frame callback when the number of mounted useTimestamp hook changes from 0 to 1. Similarly, we disable the frame callback when the number of mounted useTimestamp hooks changes from 1 to 0.

The only concern here is regarding the components frozen with react-freeze, I'm not sure how this will behave. In such case we could just return some mock of a mutable value with a constant value to prevent updates while the component is frozen.

What do you think?

@ashfurrow
Copy link
Copy Markdown
Author

There should be one global mutable value.`

Are we sure? The current timestamps update based on their start time, and can be paused individually. I agree that developers are better off with a single hook instead of 100, but if we try to make that optimization for them we constrain what the API can do.

Personally I like the option without React context better.

Yes I agree – it's better to leave app architecture up to developers. They can use context if they really want to.

@ashfurrow
Copy link
Copy Markdown
Author

I've simplified the tests, thanks for the feedback. I chatted further with Tomasz and decided to add a warning to discourage developers from over-using this. This avoids having the library make opinionated decisions about app architecture, but we can iterate on the API too.

@tjzel tjzel self-assigned this Apr 15, 2026
Copy link
Copy Markdown
Collaborator

@tjzel tjzel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you also add a docs page for this new API? Could be in a follow-up PR.

Comment on lines +17 to +20
const frameCallback = useFrameCallback((info) => {
'worklet';
timestamp.value = info.timeSinceFirstFrame;
}, isActive);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit

Suggested change
const frameCallback = useFrameCallback((info) => {
'worklet';
timestamp.value = info.timeSinceFirstFrame;
}, isActive);
const frameCallback = useFrameCallback(({timeSinceFirstFrame) => {
'worklet';
timestamp.value = timeSinceFirstFrame;
}, isActive);

Comment on lines +23 to +24
* Lets you run a function on every frame update. This is expensive, so don't
* create multiple timers if you can use one and share it.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this was supposed to be in useTimestamp.ts

Also, maybe something less threatening:

Suggested change
* Lets you run a function on every frame update. This is expensive, so don't
* create multiple timers if you can use one and share it.
* Lets you run a function on every frame update.
*
* For best performance, prefer to re-use a single `useTimestamp` timer instead of creating multiple ones.

import { useSharedValue } from './useSharedValue';

/**
* Lets you access the current frame timestamp as a shared value.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you also add an inline link there?

Suggested change
* Lets you access the current frame timestamp as a shared value.
* Lets you access the current frame timestamp as a Shared Value.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test suite has no value as it only tests the mock implementation. useFrameCallback. I added tests for useFrameCallback in #9255. After it's merged you can add a test that checks if the .value is actually progressed across frames.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants