main
js 258 lines 5.99 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import * as React from 'react';
11 import {
12 createContext,
13 forwardRef,
14 Fragment,
15 memo,
16 useCallback,
17 useContext,
18 useDebugValue,
19 useEffect,
20 useOptimistic,
21 useState,
22 use,
23 } from 'react';
24 import {useFormState, useFormStatus} from 'react-dom';
25
26 const object = {
27 string: 'abc',
28 number: 123,
29 boolean: true,
30 null: null,
31 undefined: undefined,
32 array: ['a', 'b', 'c'],
33 object: {foo: 1, bar: 2, baz: 3},
34 };
35
36 function useNestedInnerHook() {
37 return useState(123);
38 }
39 function useNestedOuterHook() {
40 return useNestedInnerHook();
41 }
42
43 function useCustomObject() {
44 useDebugValue(object);
45 return useState(123);
46 }
47
48 function useDeepHookA() {
49 useDebugValue('useDeepHookA');
50 useDeepHookB();
51 }
52 function useDeepHookB() {
53 useDebugValue('useDeepHookB');
54 useDeepHookC();
55 }
56 function useDeepHookC() {
57 useDebugValue('useDeepHookC');
58 useDeepHookD();
59 }
60 function useDeepHookD() {
61 useDebugValue('useDeepHookD');
62 useDeepHookE();
63 }
64 function useDeepHookE() {
65 useDebugValue('useDeepHookE');
66 useDeepHookF();
67 }
68 function useDeepHookF() {
69 useDebugValue('useDeepHookF');
70 }
71
72 const ContextA = createContext('A');
73 const ContextB = createContext('B');
74
75 function FunctionWithHooks(props: any, ref: React.RefSetter<any>) {
76 const [count, updateCount] = useState(0);
77 // eslint-disable-next-line no-unused-vars
78 const contextValueA = useContext(ContextA);
79 useOptimistic<number, mixed>(1);
80 use(ContextA);
81
82 // eslint-disable-next-line no-unused-vars
83 const [_, __] = useState(object);
84
85 // Custom hook with a custom debug label
86 const debouncedCount = useDebounce(count, 1000);
87
88 useCustomObject();
89
90 const onClick = useCallback(
91 function onClick() {
92 updateCount(count + 1);
93 },
94 [count],
95 );
96
97 // Tests nested custom hooks
98 useNestedOuterHook();
99
100 // eslint-disable-next-line no-unused-vars
101 const contextValueB = useContext(ContextB);
102
103 // Verify deep nesting doesn't break
104 useDeepHookA();
105
106 return <button onClick={onClick}>Count: {debouncedCount}</button>;
107 }
108 const MemoWithHooks = memo(FunctionWithHooks);
109 const ForwardRefWithHooks = forwardRef(FunctionWithHooks);
110
111 function wrapWithHoc(
112 Component: (props: any, ref: React.RefSetter<any>) => any,
113 ) {
114 function Hoc() {
115 return <Component />;
116 }
117 // $FlowFixMe[prop-missing]
118 const displayName = Component.displayName || Component.name;
119 // $FlowFixMe[incompatible-type] found when upgrading Flow
120 Hoc.displayName = `withHoc(${displayName})`;
121 return Hoc;
122 }
123 const HocWithHooks = wrapWithHoc(FunctionWithHooks);
124
125 const Suspendender = React.lazy<() => React.Node>(() => {
126 return new Promise<{default: () => React.Node, ...}>(resolve => {
127 setTimeout(() => {
128 resolve({
129 default: () => 'Finished!',
130 });
131 }, 3000);
132 });
133 });
134 function Transition() {
135 const [show, setShow] = React.useState(false);
136 const [isPending, startTransition] = React.useTransition();
137
138 return (
139 <div>
140 <React.Suspense fallback="Loading">
141 {isPending ? 'Pending' : null}
142 {show ? <Suspendender /> : null}
143 </React.Suspense>
144 {!show && (
145 <button onClick={() => startTransition(() => setShow(true))}>
146 Transition
147 </button>
148 )}
149 </div>
150 );
151 }
152
153 function incrementWithDelay(previousState: number, formData: FormData) {
154 const incrementDelay = +formData.get('incrementDelay');
155 const shouldReject = formData.get('shouldReject');
156 const reason = formData.get('reason');
157
158 return new Promise((resolve, reject) => {
159 setTimeout(() => {
160 if (shouldReject) {
161 reject(reason);
162 } else {
163 resolve(previousState + 1);
164 }
165 }, incrementDelay);
166 });
167 }
168
169 function FormStatus() {
170 const status = useFormStatus();
171
172 return <pre>{JSON.stringify(status)}</pre>;
173 }
174
175 function Forms() {
176 const [state, formAction] = useFormState<any, any>(incrementWithDelay, 0);
177 return (
178 <form>
179 State: {state}&nbsp;
180 <label>
181 delay:
182 <input
183 name="incrementDelay"
184 defaultValue={5000}
185 type="text"
186 inputMode="numeric"
187 />
188 </label>
189 <label>
190 Reject:
191 <input name="reason" type="text" />
192 <input name="shouldReject" type="checkbox" />
193 </label>
194 <button formAction={formAction}>Increment</button>
195 <FormStatus />
196 </form>
197 );
198 }
199
200 class ErrorBoundary extends React.Component<{children?: React$Node}> {
201 state: {error: any} = {error: null};
202 static getDerivedStateFromError(error: mixed): {error: any} {
203 return {error};
204 }
205 componentDidCatch(error: any, info: any) {
206 console.error(error, info);
207 }
208 render(): any {
209 if (this.state.error) {
210 return <div>Error: {String(this.state.error)}</div>;
211 }
212 return this.props.children;
213 }
214 }
215
216 export default function CustomHooks(): React.Node {
217 return (
218 <Fragment>
219 <FunctionWithHooks />
220 <MemoWithHooks />
221 <ForwardRefWithHooks />
222 <HocWithHooks />
223 <Transition />
224 <ErrorBoundary>
225 <Forms />
226 </ErrorBoundary>
227 </Fragment>
228 );
229 }
230
231 // Below copied from https://usehooks.com/
232 function useDebounce(value: number, delay: number) {
233 // State and setters for debounced value
234 const [debouncedValue, setDebouncedValue] = useState(value);
235
236 // Show the value in DevTools
237 useDebugValue(debouncedValue);
238
239 useEffect(
240 () => {
241 // Update debounced value after delay
242 const handler = setTimeout(() => {
243 setDebouncedValue(value);
244 }, delay);
245
246 // Cancel the timeout if value changes (also on delay change or unmount)
247 // This is how we prevent debounced value from updating if value is changed ...
248 // .. within the delay period. Timeout gets cleared and restarted.
249 return () => {
250 clearTimeout(handler);
251 };
252 },
253 [value, delay], // Only re-call effect if value or delay changes
254 );
255
256 return debouncedValue;
257 }
258 // Above copied from https://usehooks.com/