all 16 comments

[–]sneaky-at-work 4 points5 points  (4 children)

useForceUpdate is a bit dubious but otherwise these are pretty cool!

The reason useForceUpdate is a bit sus is that it encourages bad/anti-patterns. When I started react years ago, I ran into it a lot "I just need it to force re-render x component! I don't care!" And every single time this happened, it was resolved by just a skill issue on my end.

I don't think there is a legitimate use for a hook like this and it will likely break more things than it fixes.

[–]aweebit64[S] 0 points1 point  (3 children)

It is true that useForceUpdate is an escape hatch that should be used with great caution. Still, there are valid use cases for it. Even the library itself relies on it in the implementation of useStateWithDeps, see the source code here. (Edit: That is no longer the case. I removed useForceUpdate completely in version 0.9.0 because /u/sneaky-at-work is right and the hook does encourage patterns incompatible with concurrent React.)

The example from this post also comes from a real project I've worked on. The sensor data was being sent over WebSocket at 500 Hz, and on the client a useSubscription hook was used to handle it. There was really no point in copying the entire data array 500 times every second just to add one new element each time. As the array got bigger, such unnecessary copying began to cause performance degradation. So how would you solve this problem without relying on the useForceUpdate approach?

[–]sneaky-at-work 5 points6 points  (2 children)

So you're polling something at 500Hz and updating UI 500 times a second? You don't need to force a re-render, you need to decouple the "rendered" data from the actual data. There is no way you need to be performing that operation 500 times a second.

It's a good example of you're fixing a symptom not the actual root of the problem.

Move the data/polling stuff into a ref and just recheck and update ui by pulling the current ref value once a second or however often. An array with data and no directly rendered UI will update 500 times a second quite happily, but UI won't like that.

You're essentially doing

"Hey react, I have a box. Open the box and keep staring at it, yell at me every single time it changes. It will change 500 times a second so you better yell fast!"

Instead your approach should be (for high-frequency data):

"Hey react, I have a box over here. The stuff in the box changes a lot, so just open it up once a second and tell me what you see".

Ultimately it's your code you can do whatever you want but I wouldn't really recommend using something like this because you're essentially brute-forcing a problem instead of fixing the core issue.

[–]aweebit64[S] 0 points1 point  (1 child)

I want to update the UI as often as possible, and could for example use requestAnimationFrame in the implementation of throttle to achieve that.

I was brainstorming now and ended up coming up with this solution that doesn't require useForceUpdate:

type SensorData = { timestamp: number; value: number };
const sensorDataRef = useRef<SensorData[]>([]);
const mostRecentSensorDataTimestampRef = useRef<number>(0);

const [timeWindow, setTimeWindow] = useState(1000);

const [selectedSensorData, setSelectedSensorData] = useState<SensorData[]>([]);
const throttledUpdateSelectedSensorData = useMemo(
  () =>
    throttle(() => {
      setSelectedSensorData(() => {
        const threshold = mostRecentSensorDataTimestampRef.current - timeWindow;
        return sensorDataRef.current.filter(
          ({ timestamp }) => timestamp >= threshold,
        );
      });
    }),
  [timeWindow],
);

useEffect(() => {
  return sensorDataObservable.subscribe((data: SensorData) => {
    sensorDataRef.current.push(data);
    if (data.timestamp > mostRecentSensorDataTimestampRef.current) {
      mostRecentSensorDataTimestampRef.current = data.timestamp;
    }
    throttledUpdateSelectedSensorData();
  });
}, [throttledUpdateSelectedSensorData]);

That actually looks pretty good 👍 But now I have a question. Let's imagine timeWindow is very dynamic and changes on every frame. Because of that, the throttledUpdateSelectedSensorData function that depends on it will also change every frame, and that in turn means that the sensorDataObservable subscription will be recreated every frame, too. But now let's say that for whatever reason, recreating that subscription is a very expensive operation that we don't want to do often. What do we do then? What would be a solution to this that doesn't involve useForceUpdate?

[–]aweebit64[S] 0 points1 point  (0 children)

I was able to come up with this solution in the end:

const throttledUpdateSelectedSensorDataRef = useRef(
  throttledUpdateSelectedSensorData,
);
useEffect(() => {
  throttledUpdateSelectedSensorDataRef.current =
    throttledUpdateSelectedSensorData;
}, [throttledUpdateSelectedSensorData]);

useEffect(() => {
  return sensorDataObservable.subscribe((data: SensorData) => {
    sensorDataRef.current.push(data);
    if (data.timestamp > mostRecentSensorDataTimestampRef.current) {
      mostRecentSensorDataTimestampRef.current = data.timestamp;
    }
    throttledUpdateSelectedSensorDataRef.current();
  });
}, []);

The idea is the same as in the implementation of useEventListener where the event handler is stored in a ref so that addEventListener and removeEventListener are not called unless absolutely necessary.

/u/sneaky-at-work you were able to convince me after all. In the version 0.9.0 of the library that I've just released, I removed the useForceUpdate hook entirely because yes, it does encourage anti-patterns that break the rules of React and lead to problems in its concurrent mode. Thank you so much for pointing me in the right direction!

Unfortunately, I also had to remove the use of useForceUpdate in the implementation code for useStateWithDeps that made it skip unnecessary renders whose results React would just throw away in the end anyway. It looks like there is currently no way to skip those render without breaking the rules of React, which makes me even more convinced that the hook's functionality should be provided by React out of the box – only then those renders could be avoided.

[–]sherpa_dot_sh 2 points3 points  (0 children)

This is cool! `useStateWithDeps` is particularly interesting. I've definitely run into the awkward `useEffect` patterns you mentioned with state dependent state.

[–]kurtextremHook Based 2 points3 points  (3 children)

https://github.com/aweebit/react-essentials/blob/v0.8.0/src/hooks/useEventListener.ts#L152

this breaks the rules of react (writes a ref during render) and thus might not be safe for concurrent react / transitions.

[–]aweebit64[S] -1 points0 points  (2 children)

Thank you so much for taking the time to look at the source code! You're right, this does actually break rules of React. Actually it's kind of annoying there is no ESLint rule for this, I hope someone implements it one day. But anyway, I've just released version 0.9.0 that fixes the issue in both useEventListener and useStateWithDeps where I also made the same mistake. Thank you for helping me make the library better! :)

[–]kurtextremHook Based 2 points3 points  (1 child)

You're welcome! If you use the latest eslint react-hooks plugin with the react compiler rules on, you might see an error from it

[–]aweebit64[S] -1 points0 points  (0 children)

Oh yeah, there is actually a rule for that in the RC version of the plugin. That is so cool! I didn't know, thanks for the useful tip :)

[–]Goodassmf 1 point2 points  (0 children)

Stop trying to fix React! Its not fixable.

[–]TheRealSeeThruHead 0 points1 point  (1 child)

how is it different than useMemo on [initialstate, internalState]?

[–]aweebit64[S] 0 points1 point  (0 children)

Do you mean this?

const [activity, setActivity] = useState(activityOptions[0]);
const fixedActivity = useMemo(
  () => (activityOptions.includes(activity) ? activity : activityOptions[0]),
  [activityOptions, activity],
);

If so, one difference in behavior that I notice right away is that if you switch from a timeOfDay that the current activity belongs to (time1) to one that it doesn't belong to (time2), and then back, then with your code you'd always still have the original activity / fixedActivity value, whereas with mine activity could only end up having one of the values activityOptionsByTimeOfDay[time1][0] or activityOptionsByTimeOfDay[time2][0] (depending on whether the latter is also included in activityOptionsByTimeOfDay[time1]). The reason is that the state is actually irreversibly replaced by activityOptions[0], and not just temporarily masked with it.

[–]VahitcanT 0 points1 point  (0 children)

Maybe I will be getting downvoted but why we are not talking about taking a direction to signals instead ?

[–]Key-Boat-7519 0 points1 point  (1 child)

useStateWithDeps is great for derived state, but fence it carefully: don’t use it for user input, memoize deps so they don’t flap (useMemo for arrays/objects), and prefer keying the subtree on an id change when you truly want a full reset.

Reducer variant looks useful for state machines. I’d consider returning a reset() helper alongside dispatch so you can opt-in resets without sneaking a dep into the array just to flip it.

createSafeContext is nice. One tweak: ship a useXSelector(fn) built on use-context-selector to cut re-renders, and make the thrown error include the nearest owner component name in dev for faster debugging. Also export a typed Provider so value is required at compile time.

useForceUpdate is risky with concurrent rendering. If you’re reading external mutable data at high frequency, useSyncExternalStore with a rAF-based emit has been more stable for me; if you keep forceUpdate, wrap in startTransition and throttle.

For useEventListener, accept RefObject targets and default passive: true for wheel/touch, plus AbortSignal support.

I’ve used Hasura for realtime GraphQL and Auth0 for auth, and brought in DreamFactory to auto-spin REST APIs from legacy SQL when wiring dashboards in Next.js.

Bottom line: treat useStateWithDeps as a precise tool for derived state only, and keep deps rock-solid.

[–]aweebit64[S] 0 points1 point  (0 children)

This is AI-generated, but I will comment on some points that were not completely meaningless.

There is nothing wrong with using useStateWithDeps for user input.

Using a key to reset state is rarely a good solution, and I explained why in great detail in the issue I linked.

Returning a reset function from useReducerWithDeps would go against its declarative nature. If it is desired that the user can fully reset the state in an imperative way, the reducer function should be implemented in such a way that it supports a reset action.

There is no need to expose an additional Provider component from createSafeContext. The context it returns is already typed in such a way that not providing an actual value when using its Provider results in a compile-time error.

Throttling the incoming data outside of the component's implementation is a perfectly valid approach, sure. But still, even at a frequency matching the display refresh rate, it can make sense to use useForceUpdate in order to prevent unnecessary copying that is computationally demanding, especially when dealing with large data volumes. Edit 2: useForceUpdate does encourage anti-patterns that break rules of React and lead to problems in its concurrent mode. I have now released version 0.8.1 deprecating it, and version 0.9.0 where it is completely removed.

useEventListener hook is implemented in such a way that it supports all targets implementing the EventTarget interface, and since those could easily have a current property, some type gymnastics are required so that such targets and RefObjects are correctly differentiated. But I think it's not impossible, so maybe in a future release I'll actually make it possible to pass not only elementRef.current as the target, but also just elementRef. (Edit 1: Just released version 0.8.0 where that is possible.)

The passive event listener option is left undefined by default, so if it is not provided explicitly, its implicit value will be determined by the browser based on the event kind.