Hacker News new | ask | show | jobs
by nine_k 568 days ago
React comes with useState, useMemo, and useCallback, which is actually enough, but it may be too low-level when you think e.g. in terms of a huge interactive form. It's easy to write your own useWhatever based on these which would factor out your common boilerplate.

I suspect HTMX also does not come with every possible battery included, judging by the proliferation of libraries for HTMX-based projects. Modularity is a strength.

1 comments

it is enough in the sense that NAND gates are enough to build computers. Yes, you can write a complex application using only those, and yes it's easy to write hooks to keep the boilerplate low - although Context still feels like too much boilerplate - but as complexity grows it's quite natural to want to share state between distant parts of the application, and then you're left choosing between lifting staten and prop drilling (not great) or Context and massive, frequent rerenderings. Hence the need for third party state management solutions
Are you sure contexts causing rerenders is not solvable by moving useContext() calls into hooks that each return only the part of the context that is required and ensure reference equality of returned value (meaning any component will not have a reason to rerender unless there is an actual change in that part of the context)?
Also, should not memo [1] suffice?

[1]: https://react.dev/reference/react/memo

I thought about it more and my reasoning is as follows (I may be wrong):

— useContext() returns an object. This object is new any time anything in the context changes. If the object has a nested sub-object, it may be new as well even if it did not change, though I suppose it may depend on how context provider works.

— All components where you use that context therefore will render any time it changes. (Takeaway 1: apply loose coupling & high cohesion principle to contexts, such that if you use the context and it changes in any way there is a high chance the change is relevant to wherever you use the context.)

— The render at that stage may be fine[0], especially if contexts are nicely organized, but care is needed because a downstream child that receives a nested sub-object from the context may render as well even if the sub-object is unchanged but referentially new (unless the child is wrapped in memo() and the memo handles reference equality, which may well be what you meant). (Takeaway 2: always remember that JavaScript is full of pointers and referential equality is important in React.)

— However, if part of the context is useMemo()ed for reference equality before being passed to a child, then the child will not have a reason to render from other unrelated context changes.

[0] It may make sense not to use context in large numbers of downstream leaf components (e.g., not use it in an item rendered in a map, but use it in parent list and pass relevant props to list items).

This may be frustrating to deal with in a large project, but it may be that the effort put into organizing contexts strategically and using them with care would lead to a more solid, refactorable and reusable architecture compared to state sprinkled around the place as essentially an equivalent of global variables. It depends.

Not sure, assuming a change in context through useContext() directly counts as state change and memo does not prevent re-renders on state changes…

Generally, re-renders should not be a problem (assuming nothing changed for this component it is a no-op as far as its DOM is concerned), but that is a separate issue, I suppose. I did have to worry about re-renders on a few occasions (and it never feels great when you put effort into memoing each prop for reference quality, but something still causes a rerender from within).

honestly, I'm not sure.

This is my main complaint about React - the "just don't worry about rerenders!" model works well until it really does not, but then you're left with very little help from the tooling to understand and fix it: "why did this render happen" is still a surprisingly difficult question to answer, and if you really want to take control of this you have to very carefully micro manage useCallbacks, useMemos, memo(), probably lie about your useEffect dependencies, check every single hook, and hope that your dependencies do the same. In the words of Ben Lesh[1]: React is not a pit of success.

That said, I fear your solution would not work - your usePartOfTheContext() would re-render every time the useContext() inside did, not helping with avoiding re-renders. But if you only passed the part of context to descendants that use memo(), it _should_ work. Having children of context providers always use memo() is probably a good rule of thumb.

This uncertainty is why I find it much more productive to just slap shared state inside Jotai, so I can be reasonably certain that rerenders will have the smallest granularity without any more work.

I am very hopeful about the compiler, which should help a lot with this, freeing a lot of mental bandwidth, but also useEffectEvent() which will finally make useEffect sane.

[1]: https://x.com/BenLesh/status/1845638051424587879 worth opening for the meme in the quoted tweet

> your usePartOfTheContext() would re-render every time the useContext() inside did, not helping with avoiding re-renders

If a hook returns the same value with a stable reference across renders, and it is passed as a prop to some downstream components, it does not matter whether the hook itself uses context or not: for downstream components, prop did not change and no render can be triggered.

but downstream components would still need to be wrapped in memo to do avoid rendering if the prop did not change!
Thanks, I was not correct. Still, in my experience the impact from failing to ensure referential equality of props is usually a root cause of many issues.

If you make sure prop references are stable as early as possible, then if you run into poor performance you can always just wrap components in memo() (or some would just memo() all the things by default), but you may not even need it because renders also get cheaper when dependency diffing is effective and every hook does less work.

If prop references are unstable, things get messy in many ways.