From e549ce96280c25ed526573cb0069082cf6e800d7 Mon Sep 17 00:00:00 2001 From: tkow Date: Sun, 6 Feb 2022 22:10:57 +0900 Subject: [PATCH] feat: create react-inner-hooks rfc --- text/0000-inner-hooks.md | 313 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 text/0000-inner-hooks.md diff --git a/text/0000-inner-hooks.md b/text/0000-inner-hooks.md new file mode 100644 index 00000000..d1f53601 --- /dev/null +++ b/text/0000-inner-hooks.md @@ -0,0 +1,313 @@ +- Start Date: 2022-02-06 +- RFC PR: +- React Issue: + +# Summary + +Add a special props named innerHooks to React.Component and extend createElement. + +# Basic example + +Giving that, you want child prop using hooks combined with redux state and define useOptions as a hook followed by example. + +```tsx +export function useOptions = () => { + const users = useSelector() + return useMemo(() => { + return users.map((user) => { + return { + id: user.id, + label: user.name + } + }) + }, [users]) +} +``` + +```tsx +import { useOptions } from "./useOptions"; + +const Example = (props) => ( + + ({ + options: useOptions(), + })} + /> + +); +``` + +nearly equals to + +```tsx +import { useOptions } from "./useOptions"; + +const Example = (props) => { + const options = useOptions(); + return ( + + + + ); +}; +``` + +but, they are different about execution scopes. The formar is in child scope and the latter is in parent scope. +In addition, innerHooks prop must return partial props of the component. + +# Motivation + +Component often needs conditional renderning but hooks must be written before their even if they don't depend on the condition for [idempotent calling rule of hooks](https://reactjs.org/docs/hooks-rules.html). + +OK: + +```tsx +const Example = (props) => { + const options = useOptions(); + const { initialized, data } = useFetchData(); + if (!initialized) return null; + return ; +}; +``` + +Bad: + +```tsx +const Example = (props) => { + const { initialized, data } = useFetchData(); + if (!initialized) return null; + const options = useOptions(); + return ; +}; +``` + +or + +```tsx +const Example = (props) => { + const { initialized, data } = useFetchData(); + if (!initialized) return null; + return ; +}; +``` + +This is not problem when component is small, but big one is tough to read. + +```tsx +const Example = (props) => { + const options = useOptions() + const [optionValue, setOptionValue] = useState() + const {initialized, data} = useFetchData() + const someValue = '' + if (!initialized) return null + return ( + + + + + + + + + + { + const [optionValue, setOptionValue] = useState() + const options = useOptions() + return { + options, + value: optionValue, + onChange: setOptionValue + } + }} + otherProps + /> + {/* ...Omitted */} + + ) +``` + +And this is more useful to move the component to other place compared to the past. This example may be meaningless because state created by useState scope is very limited. It's for sake of simplicity. If you use wide scope state and handler like redux or recoil or widely-context, you'll find this feature powerful. Now we can use hooks in context instead of extracting components per container or context's consumer which tends to become a source of trouble about optimizing rendering and readability. For example, we can write context to the extent of root component scope without separationg files. + +```tsx +const Context = createContext () +const Example = (props) => { + // ... Omitted + const {initialized, data} = useFetchData() + if (!initialized) return null + return ( + + {/* ...Assuming a vast of */} + { + const {value} = useContext(Context) + return { + value, + } + }} + /> + {/* ...Assuming a vast of */} + + ) +``` + +This can limit context scope and are easier to know and extract loosely-coupled component compared to followed by the context example, we could just only use provider in the past, and it's easy to miss how many we set curly braces for deep nests. + +```tsx +const Context = createContext () +const Example = (props) => { + // ... Omitted + const {initialized, data} = useFetchData() + if (!initialized) return null + return ( + + + { + ({value}) => { + return ( + <> + {/* ...Assuming a vast of */} + { + const {value} = useContext(Context) + return { + value, + } + }} + /> + {/* ...Assuming a vast of */} + + ) + } + } + + + ) +``` + +There is simular problem about readability even if you use consumer per components. + +# Detailed design + +The innerHooks is called in intermediate layer from parent and child. +React render function create component only call hooks and merge props passed by innerProps. + +```tsx +function InterMediate(props) { + const propsFromInnerHooks = innerHooks && innerHooks() + return +} +``` + +In api level, we may be able to optimize performance. + +```tsx +React.createElement(Hello, { innerHooks: someHandleHookFunction }, null); + +function createElement(Component, props, ...children) { + if (props.innerHooks) { + props = merge(props, props.innerHooks()); + } + // continue to original function process +} +``` + +# Drawbacks + +- possible to be worse rendering performance by intermediate hooks +- parent scope variables included in innerHooks can perfome unexpected side effects. +- type inference is more complicated, it needs returned type of innerHooks and exclude them from original props if it injected + +# Alternatives + +No idea. + +# Adoption strategy + +- This does not have any breaking changes. It can ignore if there is not an innerHooks prop, unless users name a prop innerHooks. +- React typing library like flow and typescript should change component type so that infer props type when exists the innerHooks prop. + +# How we teach this + +The terminology is innerHooks. + +We may need only adding innerHooks usage the React Hooks entry of document. + +And introduction as new feature is enough. + +# Unresolved questions + +- How to imprement innerHooks during rendering. +- I may not consider enough how extent innerHooks side effects by innerHooks of decendants from parent have bad effects.