hooks.macro
hooks.macro copied to clipboard
Rules for property accesses
Because accessing a property in a callback could reference different values that the ones are available at function definition, we currently do not add property accesses to the inputs array. For example:
function MyComponent({ config }) {
return useAutoMemo(config.value * 2);
}
Gives this output:
function MyComponent({ config }) {
return useMemo(() => config.value * 2, [config]);
}
This is a big issue when dealing with refs, which are a { current }
object. Since they are mutable, this makes the problem non-trivial.
Note: the
props
reference is a special case of this. Since is easier to identify, we should at least treat that some love.
Idea: When accessing .current
on a variable, it must be explicitly labelled or inferred to either be a ref or a non-ref. Assignment to useRef
would automatically label it as a ref, as would wrapping it in the new macros markRef
and markNonRef
.
Triggers warning:
function App({ myRef /* is ref: unknown */ }) {
useAutoEffect(() => {
myRef.current
})
return null
}
Does not trigger warning:
function App({ myRef }) {
markRef(myRef) // is ref: true
useAutoEffect(() => {
myRef.current
})
return null
}
function App({ myRef }) {
markNonRef(myRef) // is ref: false
useAutoEffect(() => {
myRef.current
})
return null
}
function App() {
const myRef = useRef() // is ref: true
useAutoEffect(() => {
myRef.current
})
return null
}
function App({ myRef /* is ref: unknown */ }) {
const { current } = myRef
useAutoEffect(() => {
current
})
return null
}
If the default behavior is to add the object to dependencies and show a warning, it would probably not break a lot of code.
The algorithm
- If
.current
is accessed in the scope or child scope of an auto hook- Find its highest parent (
foo.bar.current
->foo
) - Determine status of highest parent.
- If
maybeRef
, throw error. - If
isRef
, add top parent to dependencies. - If
isNonRef
, add property access to dependencies.
- If
- Find its highest parent (
Determine status
- If defined in hook:
nonRef
- Go to component function scope
- If defined by
React.useRef
:isRef
- If defined by
React.use*
:isNonRef
- If called with
markRef
in component function scope:isRef
- If called with
markNonRef
in component function scope:isNonRef
- If defined by
- Otherwise:
maybeRef
Error message
Could not determine if ${expression} was ref or not.
Include `markRef(${topParent})` or `markNonRef(${topParent})`
in the function body to specify whether `${topParent}.*.current`
or is a React ref.
markRef/markNonRef
- For each argument:
- Assert that it's an identifier and that it is in scope
- Remove call expression
Is it possible to have useAutoMemo()
generate a variable with a munged name and then use that variable when memoizing?
e.g.
function MyComponent({ config }) {
const __asdfanasdfjl__ = config.value;
return useMemo(() => __asdfanasdfjl__ * 2, [__asdfanasdfjl__]);
}
@neoncube2 yeah that is the plan, the problem is that accessing a property is not pure/idempotent (think property getters) and some ordering may need to be respected.
@adrianhelvik your approach is actually very clean! I’ll look into it! (Sorry for the huge delay!)
No worries. That's great news! 🙂
Thanks, @yuchi. I wonder if the ordering really needs to be respected, though? My understanding is that render()
is already supposed to be pure, so it seems like we should be able to access properties as many times as we like without side effects 🙂
@neoncube2 Yes, render functions are pure, but callbacks and effect callbacks can (and usually indeed are) impure.
Some important things to consider is conditionals, this
-context and impure getters. This contrived example:
const [config, configure] = useState()
const ref = useAutoCallback(element => {
configure({
actions: {
click() { element.click() }
}
})
})
useAutoEffect(() => {
if (!element) return
config.actions.click()
})
return <button ref={ref}>Click me!</button>
(Train of thought)
Would break if transpiled into:
var _click = config.actions.click
useEffect(() => {
if (!element) return
_click()
}, [_click])
An alternative would be to transpile into:
var _click = config?.actions?.click
useEffect(() => {
if (!element) return
_click()
}, [_click])
But that would change the this
context, if the ref looked like this for example:
const ref = useAutoCallback(element => {
configure({
element,
click() { this.element.click() }
})
})
So the original input...
Should leave us with something like:
// ...
var _click = config?.actions?.click
useEffect(() => {
if (!element) return
_click.call(config.actions)
}, [element, _click])
// ...
Edit: Then again, I don't think supporting impure getters should be the highest priority as it complicates the implementation for minimal gain. Without support for impure getters, there will be no need to handle this
-context.
// ...
useEffect(() => {
if (!element) return
config.actions.click()
}, [element, config?.actions?.click])
// ...