Provide a hook that can be used to declare the relevant condition's a value's "equality" should change from the perspective of hooks using that value.
This RFC is about a potential common convention. I do not anticipate this RFC making a change to React. However I do wish to start a discussion about whether this pattern is a good idea, or what the best solution to these issues are.
Basic example
// Example hook representing any auth system someone may use, returns the current user's auth token and userId
const = ;
// Proposed hook
// - apiTokenRef will always contain the current value in `token`
// - however apiTokenRef will be referentially identical unless `userId` changes
const apiTokenRef = ;
// Crude example representing an API fetching effect
// Because apiTokenRef does not change as long as userId is the same, fetchSelf will not be re-executed if the token is just refreshed
// However because it does change when userId changes, fetchSelf will re-execute if the user signs in to a different account
const = ;
const fetchSelf = ;
;
Motivation
Thanks to the react-hooks/exhaustive-deps lint rule the dependencies of hooks are commonly managed automatcally and we are warned when we are missing a dependency that would cause an effect to not run when conditions change.
However occasionally there are circumstances where the referential equality checks used in deps do not match the conditions we actually want effects to be re-run on change of.
Currently when this issue arrises most developers workaround it by using // eslint-disable-next-line react-hooks/exhaustive-deps to disable the lint rule for one hook the author wishes to explicitly declare the change conditions for. This is unfortunate because if the hook in question has deps other than the one you want to control the conditions of, you will no longer be warned if a dep is missing. Additionally it should generally be considered bad to have to disable lint rules as as method of declaring intent.
Here is an example of that from the axios-hooks library:
config = React.
However sometimes this issue goes beyond simply wanting to ensure that a dep does not change referential equality when the value is practically the same. Sometimes you have a value that can functionally change in a way you do want a callback to recieve the new value of, but do not want to result in any effects dependent on that callback to re-run because of.
A good example of this is API auth tokens. If an auth token is refreshed before it expires you want any callbacks that would call an API with that token to have access to the new token. But you do not want an effect (or Suspense fetching) to trigger another API call for the same data.
However if an auth token changes because the user has logged out or switched user accounts, then you do want effects that cause API calls to refetch with the new token because the content or the user's permissions to see that content has changed.
Detailed design
The proposed implementation of this pattern would be a hook with this pattern:
const ref = ;
valueis the value we wish to declare the change conditions for.depsis a standard deps array. However exhaustive-deps does not manage it because we are explicitly declaring the condition that referential equality should change.refis aRefthat will act as our "value with defined deps"- As long as deps are unchanged
refwill always return the same ref, this will ensure that the deps of hooks using it do not change. - The
currentvalue ofrefwill be kept up to date with the current value ofvalue. - When
depschange a new ref is returned so the deps of hooks depending on this value will change. This also ensures that old callbacks dependent on the old value do not recieve the new value.
In theory the following code should fulfill these rules.
Drawbacks
- Using refs results in a need to use
.currentmore regularly than typical. - Because refs need to be updated in effects, I do not know if there are any race conditions where an effect may run before the effect that updates the ref to the current value.
Alternatives
-
The current status quo is still functional. Developers are able to disable lint rules when they need to declare explitict dependencies. And they can still manually define refs.
However I think it is preferential to be able to declare deps without disabling linting rules. And it should be easier to manage these types of refs when needed.
-
Some of this could be done as part of react by making the
depsarray "smarter" allowing it to be passed special values that explicitly declare change conditions. But this implementation would probably be more complex than it should be. -
Explicit dependencies could also be implemented by standardizing a series of special comments that declare explicit alterations to the dependency list. Which the lint rule could use to still run while avoiding altering deps arrays in ways that would change the intended result.
-
The issue of needing to use something other than referential equality for a value could also be solved by proving a hook that would memoize a single value based on an equality function instead of a deps array. e.g.
options = ;
Adoption strategy
If approved I would like to see this hook available in a common location for everyone building web apps and libraries use. Either in react or in another popular hooks library that some people may already have installed.
How we teach this
It may be worth documenting this pattern in the react hooks documentation to educate people about it. Perhaps in an an "alternatives to breaking the rules of hooks" section which could document alternatives to disabling exhaustive deps and common mistakes in hooks. And the react-hooks/exhaustive-deps rule documentation should probably reference this.
I expect better names for terminology and the hook(s) may show up in documentation. But I recommend using terminology that implies you are declaring the attributes of a value which are relevant to that value changing.
Unresolved questions
- The best naming for the hook is unknown
- The proposed hook is probably overcomplicated for simpler scenarios like how
axios-hooksonly needed memoize to ensure a value was still referentially identical if a config object had not actually changed settings. Should we define other hooks for these scenarios? - Where should this hook be published. In react, a common hooks library. or a dedicated library with shared maintenance?