react
react copied to clipboard
[compiler] Support annotating hook factories that produce stable hooks
What kind of issue is this?
- [ ] React Compiler core (the JS output is incorrect, or your app works incorrectly after optimization)
- [ ] babel-plugin-react-compiler (build issue installing or using the Babel plugin)
- [ ] eslint-plugin-react-compiler (build issue installing or using the eslint plugin)
- [ ] react-compiler-healthcheck (build issue installing or using the healthcheck script)
Link to repro
nope
Repro steps
The application I'm developing follows a specific pattern for accessing Redux store data based on parameters. Here's an example:
const ProductTile = ({ productId }) => {
const useProductSelector = createUseSelectorWithParam(productId)
const title = useProductSelector(productStore.selectLabel)
const value = useProductSelector(productStore.selectValue)
const brandId = useProductSelector(productStore.selectBrandId)
const useBrandSelector = createUseSelectorWithParam(brandId)
const subTitle = useBrandSelector(brandStore.selectLabel)
return <Tile title={title} value={value} subTitle={subTitle} />
}
We currently use this "higher-order function" (HOF) approach in several thousand places across the codebase, with code like the following:
export const createUseSelectorWithParam = (param) => {
const useSelectorWithParam = (selector) =>
useSelector((state) => selector(state, param))
return useSelectorWithParam
}
This approach reduces code complexity (fewer arrow functions) and enhances team productivity by decreasing informational noise. However, it currently lacks compatibility with the new React Compiler.
Question: Is there a way to inform the React Compiler that the result of createUseSelectorWithParam should be treated as a stable hook?
We're hesitant to replace thousands of instances across the codebase with arrow functions or to add parameters universally, as it would likely reduce readability (sometimes only one parameter is needed to access Redux store data, but other times two or even three are required). Additionally, with a higher number of parameters, making such extensive changes could lead to more bugs, as manually adjusting parameters in multiple places increases the chance of errors.
How often does this bug happen?
Every time
What version of React are you using?
18.2
What version of React Compiler are you using?
19.0.0-beta-6fc168f-20241025
Thanks for posting. We don't current support this pattern because it's very easy to get wrong, ie for the factory function (createUseSelectorWithParam()) to actually generate a new hook on each call and then break the rules of hooks. However, we understand that there are codebases using this pattern and we'll consider what you proposed:
Is there a way to inform the React Compiler that the result of createUseSelectorWithParam should be treated as a stable hook?
Not today, but we do have a type system and could potentially represent this. We'll consider this!
Thank you for considering this! I understand the potential issues with generating a new hook each time. In our case, we’re careful to keep the usage consistent to avoid breaking the rules of hooks. This pattern really helps us maintain readability and reduce complexity, so it’s great to know it’s on your radar for future considerations.
Thanks again for the feedback!
@josephsavona I'm actually a bit disappointed to find that hook factories seem to be unsupported in the React compiler, as they have otherwise always have seemed to work. A major new version of a library I'm working on was based around a hook factory pattern like this:
const tableFactory = createTableHelper({
// ...common options
})
//...
const table1 = tableFactory.useTable({
// specific options, inherit common options with good ts-generics
})
const table2 = tableFactory.useTable({
// specific options, inherit common options with good ts-generics
})
I guess the more correct way is just:
const commonOptions = {
// ...common options
}
//...
const table1 = useTable({
...commonOptions
// specific options
})
const table2 = useTable({
...commonOptions
// specific options
})
Which is fine, but it would still be great to be able to use generated hooks, if possible.
@KevinVandy that pattern should work if you both call createTableHelper() outside of a component/hook (ie at module scope) and also immediately destructure it. Eg const {useTable} = createTableHelper().
This makes it clear that useTable is a stable hook.
Hi, is this still open?
I am looking to fix some issues on this repo. I've previously worked on the react-native repo.