Skip to content

Commit b59a3c4

Browse files
committed
feat: create react-inner-hooks rfc
1 parent f744f67 commit b59a3c4

File tree

1 file changed

+316
-0
lines changed

1 file changed

+316
-0
lines changed

text/0000-inner-hooks.md

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
- Start Date: 2022-02-06
2+
- RFC PR:
3+
- React Issue:
4+
5+
# Summary
6+
7+
Add a special props named innerHooks to React.Component and extend createElement.
8+
9+
# Basic example
10+
11+
Giving that, you want child prop using hooks combined with redux state and define useOptions as a hook followed by example.
12+
13+
```tsx
14+
export function useOptions = () => {
15+
const users = useSelector()
16+
return useMemo(() => {
17+
return users.map((user) => {
18+
return {
19+
id: user.id,
20+
label: user.name
21+
}
22+
})
23+
}, [users])
24+
}
25+
```
26+
27+
28+
```tsx
29+
import { useOptions } from './useOptions'
30+
31+
const Example = (props) => (
32+
<Parent>
33+
<Component
34+
innerHooks={() => ({
35+
options: useOptions()
36+
})}
37+
/>
38+
</Parent>
39+
)
40+
```
41+
42+
nearly equals to
43+
44+
```tsx
45+
import { useOptions } from './useOptions'
46+
47+
const Example = (props) => {
48+
const options = useOptions()
49+
return(
50+
<Parent>
51+
<Component options={options} />
52+
</Parent>
53+
)
54+
}
55+
```
56+
57+
but, they are different about execution scopes. The formar is in child scope and the latter is in parent scope.
58+
In addition, innerHooks prop must return partial props of the component.
59+
60+
# Motivation
61+
62+
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).
63+
64+
OK:
65+
66+
```tsx
67+
const Example = (props) => {
68+
const options = useOptions()
69+
const {initialized, data} = useFetchData()
70+
if (!initialized) return null
71+
return <Component {...data} options={options} />
72+
}
73+
```
74+
75+
Bad:
76+
77+
```tsx
78+
const Example = (props) => {
79+
const {initialized, data} = useFetchData()
80+
if (!initialized) return null
81+
const options = useOptions()
82+
return <Component {...data} options={options} />
83+
}
84+
```
85+
86+
or
87+
88+
```tsx
89+
const Example = (props) => {
90+
const {initialized, data} = useFetchData()
91+
if (!initialized) return null
92+
return <Component {...data} options={useOptions()} />
93+
}
94+
```
95+
96+
This is not problem when component is small, but big one is tough to read.
97+
98+
```tsx
99+
const Example = (props) => {
100+
const options = useOptions()
101+
const [optionValue, setOptionValue] = useState()
102+
const {initialized, data} = useFetchData()
103+
const someValue = ''
104+
if (!initialized) return null
105+
return (
106+
<Component>
107+
<Child>
108+
<AnnoyedField
109+
value={someValue}
110+
onChange={someChange}
111+
class='test'
112+
otherProps
113+
/>
114+
<AnnoyedField
115+
value={someValue}
116+
onChange={someChange}
117+
class='test'
118+
otherProps
119+
/>
120+
<AnnoyedField
121+
value={someValue}
122+
onChange={someChange}
123+
class='test'
124+
otherProps
125+
/>
126+
<AnnoyedField
127+
value={someValue}
128+
onChange={someChange}
129+
class='test'
130+
otherProps
131+
/>
132+
<AnnoyedField
133+
value={someValue}
134+
onChange={someChange}
135+
class='test'
136+
otherProps
137+
/>
138+
<AnnoyedField
139+
value={someValue}
140+
onChange={someChange}
141+
class='test'
142+
otherProps
143+
/>
144+
<AnnoyedField
145+
value={someValue}
146+
onChange={someChange}
147+
class='test'
148+
otherProps
149+
/>
150+
<Select
151+
value={optionValue}
152+
onChange={setOptionValue}
153+
options={options}
154+
otherProps
155+
/>
156+
<AnnoyedField
157+
value={someValue}
158+
onChange={someChange}
159+
class='test'
160+
otherProps
161+
/>
162+
<AnnoyedField
163+
value={someValue}
164+
onChange={someChange}
165+
class='test'
166+
otherProps
167+
/>
168+
<Child/>
169+
</Component>
170+
) {...data} options={useOptions()} />
171+
}
172+
```
173+
174+
As the farther place it's used from definition, it's more tough to remember the variable name and we are forced to use editor's trick like code jump, bookmark, splited view, etc. Or scroll and switch page many times.
175+
176+
The RFC way can envelop independent hooks against others in narrower scope.
177+
178+
```tsx
179+
const Example = (props) => {
180+
// ... Omitted
181+
const {initialized, data} = useFetchData()
182+
if (!initialized) return null
183+
return (
184+
<Component>
185+
{/* ...Omitted */}
186+
<Select
187+
innerHooks={() => {
188+
const [optionValue, setOptionValue] = useState()
189+
const options = useOptions()
190+
return {
191+
options,
192+
value: optionValue,
193+
onChange: setOptionValue
194+
}
195+
}}
196+
otherProps
197+
/>
198+
{/* ...Omitted */}
199+
</Component>
200+
)
201+
```
202+
203+
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.
204+
205+
206+
```tsx
207+
const Context = createContext ()
208+
const Example = (props) => {
209+
// ... Omitted
210+
const {initialized, data} = useFetchData()
211+
if (!initialized) return null
212+
return (
213+
<Context.Provider value={{value: 'any'}}>
214+
{/* ...Assuming a vast of <AnotherFieldNoDependsOnContext /> */}
215+
<Field
216+
innerHooks={() => {
217+
const {value} = useContext(Context)
218+
return {
219+
value,
220+
}
221+
}}
222+
/>
223+
{/* ...Assuming a vast of <AnotherFieldNoDependsOnContext /> */}
224+
</Context.Provider>
225+
)
226+
```
227+
228+
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.
229+
230+
```tsx
231+
const Context = createContext ()
232+
const Example = (props) => {
233+
// ... Omitted
234+
const {initialized, data} = useFetchData()
235+
if (!initialized) return null
236+
return (
237+
<Context.Provider value={{value: 'any'}}>
238+
<Context.Consumer>
239+
{
240+
({value}) => {
241+
return (
242+
<>
243+
{/* ...Assuming a vast of <AnotherFieldNoDependsOnContext /> */}
244+
<Field
245+
innerHooks={() => {
246+
const {value} = useContext(Context)
247+
return {
248+
value,
249+
}
250+
}}
251+
/>
252+
{/* ...Assuming a vast of <AnotherFieldNoDependsOnContext /> */}
253+
</>
254+
)
255+
}
256+
}
257+
</Context.Consumer>
258+
</Context.Provider>
259+
)
260+
```
261+
262+
There is simular problem about readability even if you use consumer per components.
263+
264+
# Detailed design
265+
266+
The innerHooks is called in intermediate layer from parent and child.
267+
React render function create component only call hooks and merge props passed by innerProps.
268+
269+
```tsx
270+
function InterMediate(props) {
271+
const propsFromInnerHooks = innerHooks && innerHooks()
272+
return <Child {...props} {...propsFromInnerHooks}>
273+
}
274+
```
275+
276+
In api level, we may be able to optimize performance.
277+
278+
```tsx
279+
React.createElement(Hello, {innerHooks: someHandleHookFunction}, null)
280+
281+
function createElement(Component, props, ...children) {
282+
if(props.innerHooks) {
283+
props = merge(props, props.innerHooks())
284+
}
285+
// continue to original function process
286+
}
287+
```
288+
289+
# Drawbacks
290+
291+
- possible to be worse rendering performance by intermediate hooks
292+
- incliding parent scope variables in innerHooks can perfome unexpected side effects.
293+
- type inference is more complicated, it needs returned type of innerHooks and exclude them from original props if it injected
294+
295+
296+
# Alternatives
297+
298+
No idea.
299+
300+
# Adoption strategy
301+
302+
- This does not have any breaking changes. It can ignore if there is not an innerHooks prop, unless users name a prop innerHooks.
303+
- React typing library like flow and typescript should change component type so that infer props type when exists the innerHooks prop.
304+
305+
# How we teach this
306+
307+
The terminology is innerHooks.
308+
309+
We may need only adding innerHooks usage the React Hooks entry of document.
310+
311+
And introduction as new feature is enough.
312+
313+
# Unresolved questions
314+
315+
- How to imprement innerHooks during rendering.
316+
- I may not consider enough how extent innerHooks side effects by innerHooks of decendants from parent have bad effects.

0 commit comments

Comments
 (0)