From 308fbaad52b31ab9cf4003d680a086f81766f58d Mon Sep 17 00:00:00 2001 From: Michael Chan Date: Sat, 16 Nov 2024 00:14:48 -0800 Subject: [PATCH] keep working on examples --- .../src/pages/react19/async_useactionstate.js | 102 +++++++++ chan.dev/src/pages/react19/index.astro | 66 ++++-- .../pages/react19/prop_types_typescript.js | 76 ++++--- ...es_and_getchildcontext_with_contexttype.js | 158 +++++++++++--- .../src/pages/react19/replace_string_ref.js | 23 +- .../src/pages/react19/shiki_magic_move.tsx | 21 +- chan.dev/src/pages/react19/use_optimistic.js | 132 ++++++++++++ chan.dev/src/pages/react19/useactionstate.js | 203 ++++++++++-------- 8 files changed, 599 insertions(+), 182 deletions(-) create mode 100644 chan.dev/src/pages/react19/async_useactionstate.js create mode 100644 chan.dev/src/pages/react19/use_optimistic.js diff --git a/chan.dev/src/pages/react19/async_useactionstate.js b/chan.dev/src/pages/react19/async_useactionstate.js new file mode 100644 index 00000000..c36b5fe0 --- /dev/null +++ b/chan.dev/src/pages/react19/async_useactionstate.js @@ -0,0 +1,102 @@ +export const title = "Async Actions" +export const doc = 'https://react.dev/reference/react/useActionState' +export const playground = 'https://stackblitz.com/edit/vitejs-vite-yttb7n?file=src%2FApp.jsx' + +export const steps = + [ + ["Async Actions", + `function StatefulForm() { + const [count, incrementCount] = React.useActionState( + (previousCount) => previousCount + 1, + 0 + ); + + return ( +
+ {count} + +
+ ); +}`], + ["Make useActionState callback asynchronous", + `function StatefulForm() { + const [count, incrementCount] = React.useActionState( + async (previousCount) => previousCount + 1, + 0 + ); + + return ( +
+ {count} + +
+ ); +}`], ["Insert some async behavior into Action callback", + `function StatefulForm() { + const [count, incrementCount] = React.useActionState( + async (previousCount) => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + return previousCount + 1; + }, + 0 + ); + + return ( +
+ {count} + +
+ ); +}`], ["Destructuer isPending from third item in useActionState tuple", + `function StatefulForm() { + const [count, incrementCount, isPending] = React.useActionState( + async (previousCount) => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + return previousCount + 1; + }, + 0 + ); + + return ( +
+ {count} + +
+ ); +}`], ["Use isPending to show loading state for the form", + `function StatefulForm() { + const [count, incrementCount, isPending] = React.useActionState( + async (previousCount) => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + return previousCount + 1; + }, + 0 + ); + + return ( +
+ {count} + + {isPending && '🌀'} +
+ ); +}`], + ["✅", + `function StatefulForm() { + const [count, incrementCount, isPending] = React.useActionState( + async (previousCount) => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + return previousCount + 1; + }, + 0 + ); + + return( +
+ {count} + + {isPending && '🌀'} +
+ ); +}`] + ] diff --git a/chan.dev/src/pages/react19/index.astro b/chan.dev/src/pages/react19/index.astro index 13417e15..89a01730 100644 --- a/chan.dev/src/pages/react19/index.astro +++ b/chan.dev/src/pages/react19/index.astro @@ -2,40 +2,62 @@ import MagicMove from './shiki_magic_move' import Layout from '#layouts/Layout.astro' import * as prop_types_typescript from './prop_types_typescript' -import * as replace_contexttypes_and_getchildcontext_with_contexttype from './replace_contexttypes_and_getchildcontext_with_contexttype' -import * as replace_string_ref from './replace_string_ref' -import * as remove_module_pattern_factories from './remove_module_pattern_factories' -import * as remove_create_factory from './remove_create_factory' -import * as replace_reacttestrenderershallow_with_reactshallowrenderer from './replace_reacttestrenderershallow_with_reactshallowrenderer' -import * as replace_act_import from './replace_act_import' -import * as replace_reactdom_render from './replace_reactdom_render' -import * as replace_reactdom_hydrate from './replace_reactdom_hydrate' -import * as actions from './actions' import * as useactionstate from './useactionstate' +import * as async_useactionstate from './async_useactionstate' +import * as use_optimistic from './use_optimistic' +//import * as replace_contexttypes_and_getchildcontext_with_contexttype from './replace_contexttypes_and_getchildcontext_with_contexttype' +//import replace_string_ref from './replace_string_ref' +//import * as remove_module_pattern_factories from './remove_module_pattern_factories' +//import * as remove_create_factory from './remove_create_factory' +//import * as replace_reacttestrenderershallow_with_reactshallowrenderer from './replace_reacttestrenderershallow_with_reactshallowrenderer' +//import * as replace_act_import from './replace_act_import' +//import * as replace_reactdom_render from './replace_reactdom_render' +//import * as replace_reactdom_hydrate from './replace_reactdom_hydrate' +//import * as actions from './actions' + +// basic useOptmistic example https://react.dev/reference/react/useOptimistic#usage +// useFormStatus https://react.dev/reference/react-dom/hooks/useFormStatus +// use: https://react.dev/blog/2024/04/25/react-19#new-feature-use +// ref as prop: https://react.dev/blog/2024/04/25/react-19#ref-as-a-prop +// ref cleanup function: https://react.dev/blog/2024/04/25/react-19#cleanup-functions-for-refs +// context as provider: https://react.dev/blog/2024/04/25/react-19#context-as-a-provider +// useDeferredValue (initial value): https://react.dev/blog/2024/04/25/react-19#use-deferred-value-initial-value +// document metadata: https://react.dev/blog/2024/04/25/react-19#support-for-metadata-tags +// - stylesheets (deduped) +// - scripts (async waited) +// - preloading options function prepExamples(examples) { return Object.entries(examples).filter(([key]) => key !== "title") } --- - +
{[ - prop_types_typescript, - //replace_contexttypes_and_getchildcontext_with_contexttype, - //replace_string_ref, - //remove_module_pattern_factories, - //remove_create_factory, - //replace_reacttestrenderershallow_with_reactshallowrenderer, - //replace_act_import, - //replace_reactdom_render, - //replace_reactdom_hydrate, - //actions, - //useactionstate, + // required refactors + // prop_types_typescript, + + // refactors + // useactionstate + // async_useactionstate + use_optimistic, ].map((example) => { return ( <> - + +
+

+ Documentation + Playground +

+ {example.codemod ? {example.codemod} : null} +
) })} diff --git a/chan.dev/src/pages/react19/prop_types_typescript.js b/chan.dev/src/pages/react19/prop_types_typescript.js index 7848dc5f..d9a4298d 100644 --- a/chan.dev/src/pages/react19/prop_types_typescript.js +++ b/chan.dev/src/pages/react19/prop_types_typescript.js @@ -1,14 +1,11 @@ -// https://react.dev/blog/2024/04/25/react-19-upgrade-guide#removed-deprecated-react-apis -// npx codemod@latest react/19/prop-types-typescript - export const title = 'Migrate from PropTypes to TypeScript interface' +export const codemod = 'npx codemod@latest react/19/prop-types-typescript' +export const doc = 'https://react.dev/blog/2024/04/25/react-19-upgrade-guide#removed-deprecated-react-apis' +export const playground = 'https://stackblitz.com/edit/vitejs-vite-vw1vnh?file=src%2FApp.tsx' -export const before = - `import PropTypes from 'prop-types'; - -export function Heading({ text }) { - return

{text}

; -} +export const steps = [ + ["Migrate from PropTypes to TypeScirpt interface", + `import PropTypes from 'prop-types'; Heading.propTypes = { text: PropTypes.string, @@ -16,24 +13,43 @@ Heading.propTypes = { Heading.defaultProps = { text: 'Hello, world!', -};` +}; +export function Heading({ + text +}) { + return

{text}

; +}`], -export const replace_defaultProps_with_destructuring_assignment_default_value = - `import PropTypes from 'prop-types'; + + ["Replace defaultProps with destructuring assignment default value", + `import PropTypes from 'prop-types'; + +Heading.propTypes = { + text: PropTypes.string, +}; export function Heading({ text = 'Hello, world!' }) { return

{text}

; +}`], + + ["Replace propTypes with TypeScript interface", + `import PropTypes from 'prop-types'; + +interface Props { + text: string; } -Heading.propTypes = { - text: PropTypes.string, -};` +export function Heading({ + text = 'Hello, world!' +}) { + return

{text}

; +}`], -export const replace_propTypes_with_typescript_interface = - `import PropTypes from 'prop-types'; + ["Make the text prop optional, because we provide a default value", + `import PropTypes from 'prop-types'; interface Props { text?: string; @@ -43,10 +59,9 @@ export function Heading({ text = 'Hello, world!' }) { return

{text}

; -}` - -export const apply_typescript_interface_to_props = - `import PropTypes from 'prop-types'; +}`], + ["Apply TypeScirpt interface to props", + `import PropTypes from 'prop-types'; interface Props { text?: string; @@ -56,10 +71,20 @@ export function Heading({ text = 'Hello, world!' }: Props) { return

{text}

; -}` +}`], -export const remove_PropTypes_import = - `interface Props { + ["Remove PropTypes import", + `interface Props { + text?: string; +} + +export function Heading({ + text = 'Hello, world!' +}: Props) { + return

{text}

; +}`], + ["✅", + `interface Props { text?: string; } @@ -67,5 +92,6 @@ export function Heading({ text = 'Hello, world!' }: Props) { return

{text}

; -}` +}`] +] diff --git a/chan.dev/src/pages/react19/replace_contexttypes_and_getchildcontext_with_contexttype.js b/chan.dev/src/pages/react19/replace_contexttypes_and_getchildcontext_with_contexttype.js index 61fe61cc..fd117d4d 100644 --- a/chan.dev/src/pages/react19/replace_contexttypes_and_getchildcontext_with_contexttype.js +++ b/chan.dev/src/pages/react19/replace_contexttypes_and_getchildcontext_with_contexttype.js @@ -1,7 +1,9 @@ // https://react.dev/blog/2024/04/25/react-19-upgrade-guide#removed-removing-legacy-context +// asymettric examples -export const before = - `import PropTypes from 'prop-types'; +export const title = "Remove Legacy Context (React 19)" + +export const before = `import PropTypes from 'prop-types'; class Parent extends React.Component { static childContextTypes = { @@ -15,45 +17,88 @@ class Parent extends React.Component { render() { return ; } -} +}` -class Child extends React.Component { - static contextTypes = { +export const before10 = `import PropTypes from 'prop-types'; + +const FooContext = React.createContext() + +class Parent extends React.Component { + static childContextTypes = { foo: PropTypes.string.isRequired, }; + getChildContext() { + return { foo: 'bar' }; + } + render() { - return
{this.context.foo}
; + return ; } }` -export const remove_childContextTypes_and_getChildContext = - `import PropTypes from 'prop-types'; +export const before20 = `import PropTypes from 'prop-types'; + +const FooContext = React.createContext() class Parent extends React.Component { + static childContextTypes = { + foo: PropTypes.string.isRequired, + }; + + getChildContext() { + return { foo: 'bar' }; + } + render() { - return ; + return ( + + + + ); } -} +}` +export const before30 = `import PropTypes from 'prop-types'; -class Child extends React.Component { - static contextTypes = { +const FooContext = React.createContext() + +class Parent extends React.Component { + static childContextTypes = { foo: PropTypes.string.isRequired, }; render() { - return
{this.context.foo}
; + return ( + + + + ); } }` +export const before40 = `import PropTypes from 'prop-types'; -export const replace_childContextTypes_and_getChildContext_with_createContext = - `import PropTypes from 'prop-types'; +const FooContext = React.createContext() -const FooContext = React.createContext(); +class Parent extends React.Component { + render() { + return ( + + + + ); + } +}` +export const next10 = `import PropTypes from 'prop-types'; + +const FooContext = React.createContext() class Parent extends React.Component { render() { - return ; + return ( + + + + ); } } @@ -67,23 +112,22 @@ class Child extends React.Component { } }` -export const wrap_Context_children_in_Context = - `import PropTypes from 'prop-types'; +export const next21 = `import PropTypes from 'prop-types'; -const FooContext = React.createContext(); +const FooContext = React.createContext() class Parent extends React.Component { render() { return ( - + - ) + ); } } class Child extends React.Component { - static contextTypes = { + static contextType = { foo: PropTypes.string.isRequired, }; @@ -92,18 +136,17 @@ class Child extends React.Component { } }` -export const replace_static_contextTypes_with_static_contextType = - `import PropTypes from 'prop-types'; +export const next30 = `import PropTypes from 'prop-types'; -const FooContext = React.createContext(); +const FooContext = React.createContext() class Parent extends React.Component { render() { return ( - + - ) + ); } } @@ -115,13 +158,12 @@ class Child extends React.Component { } }` -export const remove_PropTypes = - `const FooContext = React.createContext(); +export const end = `const FooContext = React.createContext(); class Parent extends React.Component { render() { return ( - + ); @@ -132,6 +174,58 @@ class Child extends React.Component { static contextType = FooContext; render() { - return
{this.context}
; + return
{this.context.foo}
; + } +}` + +export const after = `const FooContext = React.createContext(); + +function Parent() { + return ( + + + + ); +} + +class Child extends React.Component { + static contextType = FooContext; + + render() { + return
{this.context.foo}
; } }` + +export const after20 = `const FooContext = React.createContext(); + +function Parent() { + return ( + + + + ); +} + +class Child extends React.Component { + const context = React.use(FooContext); + + render() { + return
{context.foo}
; + } +}` + +export const after30 = `const FooContext = React.createContext(); + +function Parent() { + return ( + + + + ); +} + +function Child() { + const context = React.use(FooContext); + + return
{context.foo}
; +}` diff --git a/chan.dev/src/pages/react19/replace_string_ref.js b/chan.dev/src/pages/react19/replace_string_ref.js index 0ec945d3..a0c255d5 100644 --- a/chan.dev/src/pages/react19/replace_string_ref.js +++ b/chan.dev/src/pages/react19/replace_string_ref.js @@ -1,8 +1,11 @@ // https://react.dev/blog/2024/04/25/react-19-upgrade-guide#removed-string-refs // npx codemod@latest react/19/replace-string-ref -export const before = - `class MyComponent extends React.Component { +export default { + title: "Replace String Ref with Callback", + steps: [[ + "before", + `class MyComponent extends React.Component { componentDidMount() { this.refs.input.focus(); } @@ -11,9 +14,11 @@ export const before = return ; } }` + ], -export const replace_string_ref_with_callback = - `class MyComponent extends React.Component { + [ + "replace string ref with callback", + `class MyComponent extends React.Component { componentDidMount() { this.refs.input.focus(); } @@ -22,8 +27,11 @@ export const replace_string_ref_with_callback = return this.input = input} />; } }` + ], -export const reference_instance_property_directly = `class MyComponent extends React.Component { + [ + "reference instance property directly", + `class MyComponent extends React.Component { componentDidMount() { this.input.focus(); } @@ -31,4 +39,7 @@ export const reference_instance_property_directly = `class MyComponent extends R render() { return this.input = input} />; } -}` +}`] + + ] +} diff --git a/chan.dev/src/pages/react19/shiki_magic_move.tsx b/chan.dev/src/pages/react19/shiki_magic_move.tsx index f013ece6..420f500e 100644 --- a/chan.dev/src/pages/react19/shiki_magic_move.tsx +++ b/chan.dev/src/pages/react19/shiki_magic_move.tsx @@ -56,18 +56,17 @@ export default function MagicMove({ }) } - let [, stepCode] = steps[stepCounter] + let [stepName, stepCode] = steps[stepCounter] return (
{highlighter && (
-

{title}

-
+
+ {/*
    { - /* show stepName as a complete list. */ steps.map(([name], i) => { const stepIsPassed = i <= stepCounter @@ -77,7 +76,12 @@ export default function MagicMove({
+ */}
+

{title}

+

+ {stepCounter ? stepCounter < steps.length - 1 && {stepCounter}. : null} +

{ console.log("started"); }} onEnd={() => { console.log("ended"); }} @@ -90,13 +94,14 @@ export default function MagicMove({ />
-
+
-
- )} -
+
+ ) + } +
) } diff --git a/chan.dev/src/pages/react19/use_optimistic.js b/chan.dev/src/pages/react19/use_optimistic.js new file mode 100644 index 00000000..e6df7268 --- /dev/null +++ b/chan.dev/src/pages/react19/use_optimistic.js @@ -0,0 +1,132 @@ +export const title = "useOtimistic" +export const doc = 'https://react.dev/reference/react/useOptimistic' +export const playground = 'https://stackblitz.com/edit/vitejs-vite-mwn5sn?file=src%2FApp.jsx' + +export const steps = + [ + ["Add useOptimistic to useActionState to show next state immediately", + `function StatefulForm() { + const [count, incrementCount, isPending] = React.useActionState( + async (previousCount) => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + return previousCount + 1; + }, + 0 + ); + + return( +
+ {count} + + {isPending && '🌀'} +
+ ); +}`], + ["", + `function StatefulForm() { + const [count, incrementCount, isPending] = React.useActionState( + async (previousCount) => { + addOptimisticCount(previousCount); + await new Promise((resolve) => setTimeout(resolve, 1000)); + return previousCount + 1; + }, + 0 + ); + + const [optimisticCount, addOptimisticCount] = React.useOptimistic( + count, + (state) => state + 1 + ); + + return ( +
+ {count} + + {isPending && '🌀'} +
+ ); +}`], + + ["Use optimisticCount in render", + `function StatefulForm() { + const [count, incrementCount, isPending] = React.useActionState( + async (previousCount) => { + addOptimisticCount(previousCount); + await new Promise((resolve) => setTimeout(resolve, 1000)); + return previousCount + 1; + }, + 0 + ); + + const [optimisticCount, addOptimisticCount] = React.useOptimistic( + count, + (state) => state + 1 + ); + + return ( +
+ {optimisticCount} + + {isPending && '🌀'} +
+ ); +}`], + ["Extract shared state updater into a function", + `function addOne(number) { + return number + 1; +} + +function StatefulForm() { + const [count, incrementCount, isPending] = React.useActionState( + async (previousCount) => { + addOptimisticCount(addOne(previousCount)); + await new Promise((resolve) => sestTimeout(resolve, 1000)); + return addOne(previousCount); + }, + 0 + ); + + const [optimisticCount, addOptimisticCount] = React.useOptimistic( + count, + (state, optimisticCount) => optimisticCount + ); + + return ( +
+ {optimisticCount} + + {isPending && '🌀'} +
+ ); +}`], + + ["✅", + `function addOne(number) { + return number + 1; +} + +function StatefulForm() { + const [count, incrementCount, isPending] = React.useActionState( + async (previousCount) => { + addOptimisticCount(addOne(previousCount)); + await new Promise((resolve) => setTimeout(resolve, 1000)); + return addOne(previousCount); + }, + 0 + );s + + const [optimisticCount, addOptimisticCount] = React.useOptimistic( + count, + (state, optimisticCount) => optimisticCount + ); + + return ( +
+ {optimisticCount} + + {isPending && '🌀'} +
+ ); +}`] + ] + diff --git a/chan.dev/src/pages/react19/useactionstate.js b/chan.dev/src/pages/react19/useactionstate.js index ff455a73..a7bda024 100644 --- a/chan.dev/src/pages/react19/useactionstate.js +++ b/chan.dev/src/pages/react19/useactionstate.js @@ -1,4 +1,8 @@ -export const before = `function increment(previous) { +export const title = "useActionState"; +export const doc = 'https://react.dev/reference/react/useActionState' +export const playground = 'https://stackblitz.com/edit/vitejs-vite-bdzfpt?file=src%2FApp.jsx' + +export const steps = [["Refactor click counter to useActionState…", `function increment(previous) { return previous + 1; } @@ -15,9 +19,9 @@ function StatefulForm() { ); -}` +}`], -export const next1 = `function increment(previous) { +["Replace useState with useActionState", `function increment(previous) { return previous + 1; } @@ -35,8 +39,9 @@ function StatefulForm() { ); }` +], ["Move state update function into useActionState", -export const next2 = `function increment(previous) { + `function increment(previous) { return previous + 1; } @@ -53,9 +58,9 @@ function StatefulForm() { ); -}` +}`], -export const next3 = `function increment(previous) { +["Replace onClick with formAction", `function increment(previous) { return previous + 1; } @@ -72,9 +77,9 @@ function StatefulForm() { ); -}` +}`], -export const next4 = `function increment(previous) { +["Wrap it up in a form element", `function increment(previous) { return previous + 1; } @@ -91,7 +96,7 @@ function StatefulForm() { ); -}` +}`], // next steps: @@ -100,7 +105,7 @@ function StatefulForm() { // 3. utilize isPending hook // 4. permalink (probably different example) -export const final = `function increment(previous) { +["By convention, we call handler a formAction", `function increment(previous) { return previous + 1; } @@ -117,10 +122,9 @@ function StatefulForm() { ); -}` +}`], -// by convention, we call these formAction -export const final2 = `function increment(previous) { +["✅", `function increment(previous) { return previous + 1; } @@ -137,84 +141,105 @@ function StatefulForm() { ); -}` +}`], +] -export const after2 = `function StatefulForm() { - return ( -
Increment -
- ); -}` - -export const after1 = `function StatefulForm() { - return ( -
Increment -
- ); -}` - -export const after10 = `function StatefulForm() { - const count = 0; - - return ( -
Increment -
- ); -}` - -export const after11 = `function StatefulForm() { - const [count] = React.useActionState(, 0); - - return ( -
Increment -
- ); -}` - -export const after12 = `function StatefulForm() { - const [count, incrementCount] = React.useActionState(, 0); - - return ( -
Increment -
- ); -}` - -export const after13 = `function StatefulForm() { - const [count, incrementCount] = React.useActionState(0); - - return ( -
Increment -
- ); -}` - -// click counter with form action -export const after = `function StatefulForm() { - const [count, incrementCount] = React.useActionState( - (previousCount) => previousCount + 1, - 0 - ); +// by convention, we call these formAction +//["final2", `function increment(previous) { +// return previous + 1; +//} +// +//function StatefulForm() { +// const [count, formAction] = React.useActionState(increment, 0); +// +// return ( +//
+// {count} +// +//
+// ); +//}`], +// +//["final3", `function StatefulForm() { +// return ( +//
Increment +//
+// ); +//}`], +// +//["after1", `function StatefulForm() { +// return ( +//
Increment +//
+// ); +//}`], +// +//["after10", `function StatefulForm() { +// const count = 0; +// +// return ( +//
Increment +//
+// ); +//}`], +// +//["after11", `function StatefulForm() { +// const [count] = React.useActionState(, 0); +// +// return ( +//
Increment +//
+// ); +//}`] - return ( -
Increment -
- ); -}` +//export const after12 = `function StatefulForm() { +// const [count, incrementCount] = React.useActionState(, 0); +// +// return ( +//
Increment +//
+// ); +//}` +// +//export const after13 = `function StatefulForm() { +// const [count, incrementCount] = React.useActionState(0); +// +// return ( +//
Increment +//
+// ); +//}` +// +//// click counter with form action +//export const after = `function StatefulForm() { +// const [count, incrementCount] = React.useActionState( +// (previousCount) => previousCount + 1, +// 0 +// ); +// +// return ( +//
Increment +//
+// ); +//}` //export const before =