From 8e049c4aabe1fa53efea515f63db51a89523c8ff Mon Sep 17 00:00:00 2001 From: Caleb Stimpson Date: Sat, 12 Jul 2025 10:23:15 -0600 Subject: [PATCH 01/15] Writeup without details section --- text/0000-context-applicative.md | 148 +++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 text/0000-context-applicative.md diff --git a/text/0000-context-applicative.md b/text/0000-context-applicative.md new file mode 100644 index 00000000..810f3987 --- /dev/null +++ b/text/0000-context-applicative.md @@ -0,0 +1,148 @@ +- Start Date: 2025-07-12 +- RFC PR: (leave this empty) +- React Issue: (leave this empty) + +# Summary + +Selectors for Context are an in-demand feature, but with a slightly different interface, can be made much more universally useful. + +Following from functional programming techniques, this RFC proposes adding a: + +- .map to Contexts, of type: `(this: Context, fn: (data: T) => U) => Context` +- .apply to Contexts, of type: `(this: Context, fn: Context<(data: T) => U>) => Context` + +Every call, without respect to the identity of the given argument (function / context of functions), returns a distinct context. +To be usable within renders, existing user-land techniques like WeakMap can be used to stabilize the identity of the returned context to the identity of the given argument. + +# Basic example + +If the proposal involves a new or changed API, include a basic code example. +Omit this section if it's not applicable. + +```js +// User can set either of these for our API +const ViewportReadonly = createContext(null); +const ViewportWritable = createContext(null); +// Now ViewportRead will grab either, with a preference for ViewportWritable's +const ViewportRead = ViewportReadonly.apply(ViewportWritable.map((writableViewport) => (readableViewport) => writableViewport ?? readabelViewport)); + +function IntegrateLibrary({ viewport }) { + return ( + // User can choose which to provide + + + + ); +} + +function Library() { + // Library will pick up either, + const viewport = useContext(ViewportRead); +} +``` + + +# Motivation + +Motivation for deriving Contexts from other Contexts is already well-established. + +Motivation for this approach is that these .map and .apply functions are drawn from the Functor and Applicative Functor (respectively) from Haskell. +This style of approach is thoroughly treaded ground, being used with great success in Haskell for many years. + +To briefly describe the purpose of .apply in addition to .map (this explanation is no different from the many articles on Functor vs Applicative out in the wild): + +- With just .map we can refine one Context into any number of others, which can themselves be refined, creating a tree of ever more specific (and less informative) Contexts. + +- With .apply however, we can merge any number of Contexts into one context. +Now we can create a DAG, which is so much more expressive than just a tree. + +While I believe this interface is more pleasant in both ergonomics, efficiency and theory than wrapping `useContext`, +what I believe is particularly powerful about it is **its ability to change over time**. +If some code depends on a Context, that Context can later be rewritten in terms of other Contexts. +**The consumption is isolated from the production**: the producers can change without necessitating a change in the consumers. + +# Detailed design + +This is the bulk of the RFC. Explain the design in enough detail for somebody +familiar with React to understand, and for somebody familiar with the +implementation to implement. This should get into specifics and corner-cases, +and include examples of how the feature is used. Any new terminology should be +defined here. + +# Drawbacks + +Why should we *not* do this? Please consider: + +- implementation cost, both in term of code size and complexity +- whether the proposed feature can be implemented in user space +- the impact on teaching people React +- integration of this feature with other existing and planned features +- cost of migrating existing React applications (is it a breaking change?) + +There are tradeoffs to choosing any path. Attempt to identify them here. + +# Alternatives + +Unfortunately Applicative Functors are fairly more awkward in JavaScript than they are in Haskell (Haskell curries by default, where JS doesn't). + +The Applicative Functor interface could be made available in some other forms: + +## Via liftA2 + +Instead of a type like `.apply`'s: + +> `(this: Context, fn: Context<(data: T) => U>): Context` + +a function called something like `.with` could be exposed: + +> `(this: Context, other: Context, combine: (a: T, b: U) => R): Context` + +Note this is the same as `.map`'ing `other` by `combine` into `(data: T) => R`, and similarly `.apply` could be defined in terms of `.with`. + +**I recommend this approach**. +I think it would be more natural for JavaScript users, and for the typical use of combining two Contexts, only requires creating the output Context, +where `.apply` requires creating the intermediate Context from `.map`'ing `other`. +I didn't want to open the RFC with this, as Applicative Functors are instead usually introduced with `.apply`. + +I don't love the name `.with` either. +For reference in Haskell `.with` is called `liftA2`. + +## Via sequence + +Promises are also valid Applicative Functors. While .then does have all the power of .apply (and then some), there is a particularly Applicative-y util for Promises: `Promise.all` (_handling of TypeScript tuples omitted for brevity_): + +> `(promises: Array>): Promise>` + +This pattern is called `sequence` in Haskell. Exposing a function like: + +> `(contexts: Array>): Context>` + +would give the same power to users as `.apply`. + +However, there are a number of reasons this interface isn't that great: +- combining more than two or three Contexts in not that common, for which the Array is needless overhead +- there is no Context namespace, so it would need to be exposed as a plain function from 'react' + +However, I figured mentioning `Promise` would be useful for precedence. + +# Adoption strategy + +If we implement this proposal, how will existing React developers adopt it? Is +this a breaking change? Can we write a codemod? Should we coordinate with +other projects or libraries? + +# How we teach this + +What names and terminology work best for these concepts and why? How is this +idea best presented? As a continuation of existing React patterns? + +Would the acceptance of this proposal mean the React documentation must be +re-organized or altered? Does it change how React is taught to new developers +at any level? + +How should this feature be taught to existing React developers? + +# Unresolved questions + +Optional, but suggested for first drafts. What parts of the design are still +TBD? From afe96c1b650b41db1c205624e7c30f27bb37356a Mon Sep 17 00:00:00 2001 From: Caleb Stimpson Date: Sat, 12 Jul 2025 10:35:44 -0600 Subject: [PATCH 02/15] Rephrasing and style improvements --- text/0000-context-applicative.md | 47 ++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/text/0000-context-applicative.md b/text/0000-context-applicative.md index 810f3987..da7e01c1 100644 --- a/text/0000-context-applicative.md +++ b/text/0000-context-applicative.md @@ -4,15 +4,18 @@ # Summary -Selectors for Context are an in-demand feature, but with a slightly different interface, can be made much more universally useful. +Selectors for Context are an in-demand feature, but with a slightly different interface, can be made much more universally useful +[.slice as proposed here is similar](https://github.com/reactjs/rfcs/pull/119#issuecomment-512529871]). -Following from functional programming techniques, this RFC proposes adding a: +Following from functional programming techniques, this RFC proposes adding these methods to Context: -- .map to Contexts, of type: `(this: Context, fn: (data: T) => U) => Context` -- .apply to Contexts, of type: `(this: Context, fn: Context<(data: T) => U>) => Context` +- `.map` to Contexts, of type: `(this: Context, fn: (data: T) => U) => Context` +- `.apply` to Contexts, of type: `(this: Context, fn: Context<(data: T) => U>) => Context` -Every call, without respect to the identity of the given argument (function / context of functions), returns a distinct context. -To be usable within renders, existing user-land techniques like WeakMap can be used to stabilize the identity of the returned context to the identity of the given argument. +`.map` allows users to refine and select their Contexts, while `.apply` allows users to combine multiple Contexts together. This is convered in detail in Motivation. + +Every call, without respect to the identity of the given argument (function / context of functions), returns a distinct Context. +To be usable within renders, existing user-land techniques like WeakMap can be used to stabilize the identity of the returned Context to the identity of the given argument. # Basic example @@ -24,7 +27,9 @@ Omit this section if it's not applicable. const ViewportReadonly = createContext(null); const ViewportWritable = createContext(null); // Now ViewportRead will grab either, with a preference for ViewportWritable's -const ViewportRead = ViewportReadonly.apply(ViewportWritable.map((writableViewport) => (readableViewport) => writableViewport ?? readabelViewport)); +const ViewportRead = ViewportReadonly.apply(ViewportWritable.map((writableViewport) => (readableViewport) => { + return writableViewport ?? readabelViewport; +})); function IntegrateLibrary({ viewport }) { return ( @@ -46,20 +51,20 @@ function Library() { Motivation for deriving Contexts from other Contexts is already well-established. -Motivation for this approach is that these .map and .apply functions are drawn from the Functor and Applicative Functor (respectively) from Haskell. +Motivation for this approach is that these `.map` and `.apply` functions are drawn from the Functor and Applicative Functor (respectively) from Haskell. This style of approach is thoroughly treaded ground, being used with great success in Haskell for many years. -To briefly describe the purpose of .apply in addition to .map (this explanation is no different from the many articles on Functor vs Applicative out in the wild): +To briefly describe the purpose of `.apply` in addition to `.map` (this explanation is no different from the many articles on Functor vs Applicative out in the wild): -- With just .map we can refine one Context into any number of others, which can themselves be refined, creating a tree of ever more specific (and less informative) Contexts. +- With just `.map` we can refine one Context into any number of others, which can themselves be refined, creating a tree of ever more specific (and less informative) Contexts. -- With .apply however, we can merge any number of Contexts into one context. +- With `.apply` however, we can merge any number of Contexts into one Context. Now we can create a DAG, which is so much more expressive than just a tree. While I believe this interface is more pleasant in both ergonomics, efficiency and theory than wrapping `useContext`, what I believe is particularly powerful about it is **its ability to change over time**. If some code depends on a Context, that Context can later be rewritten in terms of other Contexts. -**The consumption is isolated from the production**: the producers can change without necessitating a change in the consumers. +**The consumption is isolated from the production**: the producers can change without necessitating the consumers read new Contexts. # Detailed design @@ -85,17 +90,21 @@ There are tradeoffs to choosing any path. Attempt to identify them here. Unfortunately Applicative Functors are fairly more awkward in JavaScript than they are in Haskell (Haskell curries by default, where JS doesn't). -The Applicative Functor interface could be made available in some other forms: +The Applicative Functor interface could be made available in some other forms instead: ## Via liftA2 Instead of a type like `.apply`'s: -> `(this: Context, fn: Context<(data: T) => U>): Context` +```ts +(this: Context, fn: Context<(data: T) => U>): Context +``` a function called something like `.with` could be exposed: -> `(this: Context, other: Context, combine: (a: T, b: U) => R): Context` +```ts +(this: Context, other: Context, combine: (a: T, b: U) => R): Context +``` Note this is the same as `.map`'ing `other` by `combine` into `(data: T) => R`, and similarly `.apply` could be defined in terms of `.with`. @@ -111,11 +120,15 @@ For reference in Haskell `.with` is called `liftA2`. Promises are also valid Applicative Functors. While .then does have all the power of .apply (and then some), there is a particularly Applicative-y util for Promises: `Promise.all` (_handling of TypeScript tuples omitted for brevity_): -> `(promises: Array>): Promise>` +```ts +(promises: Array>): Promise> +``` This pattern is called `sequence` in Haskell. Exposing a function like: -> `(contexts: Array>): Context>` +```ts +(contexts: Array>): Context> +``` would give the same power to users as `.apply`. From f15fec270eeb427595b50297ff8263af00d57062 Mon Sep 17 00:00:00 2001 From: Caleb Stimpson Date: Sat, 12 Jul 2025 10:46:49 -0600 Subject: [PATCH 03/15] Rephrasing and style improvements pt 2 --- text/0000-context-applicative.md | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/text/0000-context-applicative.md b/text/0000-context-applicative.md index da7e01c1..2ee0afe2 100644 --- a/text/0000-context-applicative.md +++ b/text/0000-context-applicative.md @@ -5,12 +5,12 @@ # Summary Selectors for Context are an in-demand feature, but with a slightly different interface, can be made much more universally useful -[.slice as proposed here is similar](https://github.com/reactjs/rfcs/pull/119#issuecomment-512529871]). +([.slice as proposed here is similar](https://github.com/reactjs/rfcs/pull/119#issuecomment-512529871])). Following from functional programming techniques, this RFC proposes adding these methods to Context: -- `.map` to Contexts, of type: `(this: Context, fn: (data: T) => U) => Context` -- `.apply` to Contexts, of type: `(this: Context, fn: Context<(data: T) => U>) => Context` +- `.map`, of type: `(this: Context, fn: (data: T) => U) => Context` +- `.apply`, of type: `(this: Context, fn: Context<(data: T) => U>) => Context` (_I actually recommend one of its alternatives_) `.map` allows users to refine and select their Contexts, while `.apply` allows users to combine multiple Contexts together. This is convered in detail in Motivation. @@ -19,17 +19,10 @@ To be usable within renders, existing user-land techniques like WeakMap can be u # Basic example -If the proposal involves a new or changed API, include a basic code example. -Omit this section if it's not applicable. - ```js // User can set either of these for our API const ViewportReadonly = createContext(null); const ViewportWritable = createContext(null); -// Now ViewportRead will grab either, with a preference for ViewportWritable's -const ViewportRead = ViewportReadonly.apply(ViewportWritable.map((writableViewport) => (readableViewport) => { - return writableViewport ?? readabelViewport; -})); function IntegrateLibrary({ viewport }) { return ( @@ -40,8 +33,12 @@ function IntegrateLibrary({ viewport }) { ); } +// Now ViewportRead will grab either, with a preference for ViewportWritable's +const ViewportRead = ViewportReadonly.apply(ViewportWritable.map((writableViewport) => (readableViewport) => { + return writableViewport ?? readabelViewport; +})); function Library() { - // Library will pick up either, + // Library will pick up either const viewport = useContext(ViewportRead); } ``` @@ -54,7 +51,7 @@ Motivation for deriving Contexts from other Contexts is already well-established Motivation for this approach is that these `.map` and `.apply` functions are drawn from the Functor and Applicative Functor (respectively) from Haskell. This style of approach is thoroughly treaded ground, being used with great success in Haskell for many years. -To briefly describe the purpose of `.apply` in addition to `.map` (this explanation is no different from the many articles on Functor vs Applicative out in the wild): +To briefly describe the purpose of `.apply` in addition to `.map` (this explanation is no different from the many articles on Functor vs Applicative Functor out in the wild): - With just `.map` we can refine one Context into any number of others, which can themselves be refined, creating a tree of ever more specific (and less informative) Contexts. @@ -118,7 +115,7 @@ For reference in Haskell `.with` is called `liftA2`. ## Via sequence -Promises are also valid Applicative Functors. While .then does have all the power of .apply (and then some), there is a particularly Applicative-y util for Promises: `Promise.all` (_handling of TypeScript tuples omitted for brevity_): +Promises are also valid Applicative Functors. While `.then` does have all the power of `.apply` (and then some), there is a particularly Applicative-y util for Promises: `Promise.all` (_handling of TypeScript tuples omitted for brevity_): ```ts (promises: Array>): Promise> From 4083de0d9bbbc8842acde47bdcf543c52fde1813 Mon Sep 17 00:00:00 2001 From: Caleb Stimpson Date: Sat, 12 Jul 2025 11:25:28 -0600 Subject: [PATCH 04/15] All but Detailed Design --- text/0000-context-applicative.md | 48 ++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/text/0000-context-applicative.md b/text/0000-context-applicative.md index 2ee0afe2..ff6b0755 100644 --- a/text/0000-context-applicative.md +++ b/text/0000-context-applicative.md @@ -73,20 +73,34 @@ defined here. # Drawbacks -Why should we *not* do this? Please consider: +## Surface area -- implementation cost, both in term of code size and complexity -- whether the proposed feature can be implemented in user space -- the impact on teaching people React -- integration of this feature with other existing and planned features -- cost of migrating existing React applications (is it a breaking change?) +Deriving values from multiple Contexts is very possible with custom hooks wrapping `useContext`. +I believe it is worth the surface area as: -There are tradeoffs to choosing any path. Attempt to identify them here. +- custom hooks wrapping `useContext` will not be allowed to be called conditionally with `use`. +- anything wanting to preserve `use` semantics could not `useMemo` to avoid expensive recomputation, where `.map` allows that expensive computation to be lifted out of renders. + +I also believe the the surface area is satisfying small, with no extra hooks or exports. + +## ReadonlyContext + +Contexts derived from other Contexts cannot have `.Provider`'s. +If derived Contexts had been around from day one of Context, I think distinguishing a `WritableContext` type would be more useful than distinguishing a `ReadonlyContext` (as not many places where the types can't be inferred would need to then provide a Context value). +I would not think a codemod to rewrite current `Context`'s to `WritableContext` would be worth it. + +However, this only affects TypeScript users, and the vast majority of usages won't have explicit type annotations. + +## Implementation complexity + +TODO: I cannot say how complicated it is, but I would think this to not be overly complicated. # Alternatives Unfortunately Applicative Functors are fairly more awkward in JavaScript than they are in Haskell (Haskell curries by default, where JS doesn't). +This feature competes with `useContextSelector`, where giving both as primitives + The Applicative Functor interface could be made available in some other forms instead: ## Via liftA2 @@ -137,22 +151,20 @@ However, I figured mentioning `Promise` would be useful for precedence. # Adoption strategy -If we implement this proposal, how will existing React developers adopt it? Is -this a breaking change? Can we write a codemod? Should we coordinate with -other projects or libraries? +A codemod for TypeScript users _could_ be made to convert existing explicit type annotations of `Context`'s to `WritableContext`. +Beyond that, everything should Just Work. # How we teach this -What names and terminology work best for these concepts and why? How is this -idea best presented? As a continuation of existing React patterns? +These methods could share one docs page, and beyond that would be out of the users way. +For the users who stumble on it while poking around: -Would the acceptance of this proposal mean the React documentation must be -re-organized or altered? Does it change how React is taught to new developers -at any level? +- `.map` is already a familiar name from Array, and +- they are completely described by their types (not preserving identity is their only non-pure semantic, and that has strong precedence from Array `.map`). -How should this feature be taught to existing React developers? +Finding an intuitive name for `.with` would be trickiest part. # Unresolved questions -Optional, but suggested for first drafts. What parts of the design are still -TBD? +- `Context` + `ReadonlyContext` vs `WritableContext` + `Context` +- Implementation complexity and performance of derivation From ca3a4e4f6ec49b7c4cd3203b2d90b6266fa9855e Mon Sep 17 00:00:00 2001 From: Caleb Stimpson Date: Sat, 12 Jul 2025 12:02:23 -0600 Subject: [PATCH 05/15] Details section --- text/0000-context-applicative.md | 78 ++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 5 deletions(-) diff --git a/text/0000-context-applicative.md b/text/0000-context-applicative.md index ff6b0755..b1cdfce6 100644 --- a/text/0000-context-applicative.md +++ b/text/0000-context-applicative.md @@ -65,11 +65,78 @@ If some code depends on a Context, that Context can later be rewritten in terms # Detailed design -This is the bulk of the RFC. Explain the design in enough detail for somebody -familiar with React to understand, and for somebody familiar with the -implementation to implement. This should get into specifics and corner-cases, -and include examples of how the feature is used. Any new terminology should be -defined here. +Types would be changed like so: + +```ts +interface ReadonlyContext { + Consumer: Consumer; + displayName?: string | undefined; +} +interface Context extends ReadonlyContext { + Provider: Provider; +} +``` + +## Semantics + +Contexts from `createContext` now have two prototype methods, `.map` and `.apply` (or `.with`). +The distinct Contexts returned from these methods do not have a `.Provider`. + +`.map` has two guarantees, the first that given: +```ts +type T = // ... +type U = // ... +declare function someMapping(data: T): U +declare function showAsJSX(data: U): ReactNode +declare const SomeContext: ReadonlyContext +``` + +this `SomeComponent`: +```ts +const MappedContext = SomeContext.map(someMapping); +function SomeComponent() { + const data = useContext(MappedContext); + return showAsJSX(data); +} +``` + +will always render the same as this `SomeComponent`: +```ts +function SomeComponent() { + const data = someMapping(useContext(SomeContext)); + return showAsJSX(data); +} +``` + +The second is that in this snippet, `Child` will never be rerendered (as least by the `useContext`). +When the map function returns the same as it did for the last push by `Object.is` semantics, the returned Context will not push an update to its subscribers. +```ts +const CountContext = createContext(0) +const ZeroContext = CountContext.map(() => 0) + +function Child() { + const zero = useContext(ZeroContext) + return
{zero}
+} +const child = + +function Parent() { + const [count, setCount] = useState(0) + return ( +
rerender(n => n + 1)}> + + {child} + +
+ ) +} +``` + +`.apply` has similar semantics with render equivalence and not pushing updates on the same value. + +## Implementation + +TODO: How is this implemented? # Drawbacks @@ -168,3 +235,4 @@ Finding an intuitive name for `.with` would be trickiest part. - `Context` + `ReadonlyContext` vs `WritableContext` + `Context` - Implementation complexity and performance of derivation +- Could default displayName of derived Contexts from input Context displayName's and the function names. From e06d68223aaed581e2ff2544accae93b283becc1 Mon Sep 17 00:00:00 2001 From: Caleb Stimpson Date: Sat, 12 Jul 2025 12:09:55 -0600 Subject: [PATCH 06/15] Mention ReadonlyContext in summary --- text/0000-context-applicative.md | 34 +++++++++++++++++--------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/text/0000-context-applicative.md b/text/0000-context-applicative.md index b1cdfce6..397d5b1b 100644 --- a/text/0000-context-applicative.md +++ b/text/0000-context-applicative.md @@ -7,10 +7,12 @@ Selectors for Context are an in-demand feature, but with a slightly different interface, can be made much more universally useful ([.slice as proposed here is similar](https://github.com/reactjs/rfcs/pull/119#issuecomment-512529871])). -Following from functional programming techniques, this RFC proposes adding these methods to Context: +Following from functional programming techniques, this RFC proposes adding: +- a `ReadonlyContext` type which is like `Context` but without `.Provider` -- `.map`, of type: `(this: Context, fn: (data: T) => U) => Context` -- `.apply`, of type: `(this: Context, fn: Context<(data: T) => U>) => Context` (_I actually recommend one of its alternatives_) +and these methods to Context: +- `.map`, of type: `(this: ReadonlyContext, fn: (data: T) => U) => ReadonlyContext` +- `.apply`, of type: `(this: ReadonlyContext, fn: Context<(data: T) => U>) => ReadonlyContext` (_I actually recommend one of its alternatives_) `.map` allows users to refine and select their Contexts, while `.apply` allows users to combine multiple Contexts together. This is convered in detail in Motivation. @@ -46,7 +48,7 @@ function Library() { # Motivation -Motivation for deriving Contexts from other Contexts is already well-established. +Motivation for selecting Contexts is already well-established by the popularity of [https://github.com/reactjs/rfcs/pull/119](RFC 119). Motivation for this approach is that these `.map` and `.apply` functions are drawn from the Functor and Applicative Functor (respectively) from Haskell. This style of approach is thoroughly treaded ground, being used with great success in Haskell for many years. @@ -84,11 +86,11 @@ The distinct Contexts returned from these methods do not have a `.Provider`. `.map` has two guarantees, the first that given: ```ts -type T = // ... -type U = // ... -declare function someMapping(data: T): U -declare function showAsJSX(data: U): ReactNode -declare const SomeContext: ReadonlyContext +type T = /* ... */; +type U = /* ... */; +declare function someMapping(data: T): U; +declare function showAsJSX(data: U): ReactNode; +declare const SomeContext: ReadonlyContext; ``` this `SomeComponent`: @@ -111,24 +113,24 @@ function SomeComponent() { The second is that in this snippet, `Child` will never be rerendered (as least by the `useContext`). When the map function returns the same as it did for the last push by `Object.is` semantics, the returned Context will not push an update to its subscribers. ```ts -const CountContext = createContext(0) -const ZeroContext = CountContext.map(() => 0) +const CountContext = createContext(0); +const ZeroContext = CountContext.map(() => 0); function Child() { - const zero = useContext(ZeroContext) - return
{zero}
+ const zero = useContext(ZeroContext); + return
{zero}
; } -const child = +const child = ; function Parent() { - const [count, setCount] = useState(0) + const [count, setCount] = useState(0); return (
rerender(n => n + 1)}> {child}
- ) + ); } ``` From 92c560efd0f8c1f6c9319310148c9fe9cd3e07fd Mon Sep 17 00:00:00 2001 From: Caleb Stimpson Date: Sat, 12 Jul 2025 12:12:24 -0600 Subject: [PATCH 07/15] Replace all Context's with ReadonlyContext's in types --- text/0000-context-applicative.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/text/0000-context-applicative.md b/text/0000-context-applicative.md index 397d5b1b..667f84bd 100644 --- a/text/0000-context-applicative.md +++ b/text/0000-context-applicative.md @@ -12,7 +12,7 @@ Following from functional programming techniques, this RFC proposes adding: and these methods to Context: - `.map`, of type: `(this: ReadonlyContext, fn: (data: T) => U) => ReadonlyContext` -- `.apply`, of type: `(this: ReadonlyContext, fn: Context<(data: T) => U>) => ReadonlyContext` (_I actually recommend one of its alternatives_) +- `.apply`, of type: `(this: ReadonlyContext, fn: ReadonlyContext<(data: T) => U>) => ReadonlyContext` (_I actually recommend one of its alternatives_) `.map` allows users to refine and select their Contexts, while `.apply` allows users to combine multiple Contexts together. This is convered in detail in Motivation. @@ -177,13 +177,13 @@ The Applicative Functor interface could be made available in some other forms in Instead of a type like `.apply`'s: ```ts -(this: Context, fn: Context<(data: T) => U>): Context +(this: ReadonlyContext, fn: ReadonlyContext<(data: T) => U>): ReadonlyContext ``` a function called something like `.with` could be exposed: ```ts -(this: Context, other: Context, combine: (a: T, b: U) => R): Context +(this: ReadonlyContext, other: ReadonlyContext, combine: (a: T, b: U) => R): ReadonlyContext ``` Note this is the same as `.map`'ing `other` by `combine` into `(data: T) => R`, and similarly `.apply` could be defined in terms of `.with`. @@ -207,7 +207,7 @@ Promises are also valid Applicative Functors. While `.then` does have all the po This pattern is called `sequence` in Haskell. Exposing a function like: ```ts -(contexts: Array>): Context> +(contexts: Array>): ReadonlyContext> ``` would give the same power to users as `.apply`. From a41d567148cd89be057a9c1d76d629653b083458 Mon Sep 17 00:00:00 2001 From: Caleb Stimpson Date: Sat, 12 Jul 2025 12:42:43 -0600 Subject: [PATCH 08/15] Mention useContextSelector --- text/0000-context-applicative.md | 33 ++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/text/0000-context-applicative.md b/text/0000-context-applicative.md index 667f84bd..92104b2f 100644 --- a/text/0000-context-applicative.md +++ b/text/0000-context-applicative.md @@ -65,6 +65,8 @@ what I believe is particularly powerful about it is **its ability to change over If some code depends on a Context, that Context can later be rewritten in terms of other Contexts. **The consumption is isolated from the production**: the producers can change without necessitating the consumers read new Contexts. +As an added bonus over wrapping `useContext`, derived Contexts could be used conditionally by `use`. + # Detailed design Types would be changed like so: @@ -166,13 +168,13 @@ TODO: I cannot say how complicated it is, but I would think this to not be overl # Alternatives -Unfortunately Applicative Functors are fairly more awkward in JavaScript than they are in Haskell (Haskell curries by default, where JS doesn't). +## Alternatives of this proposal -This feature competes with `useContextSelector`, where giving both as primitives +Unfortunately Applicative Functors are fairly more awkward in JavaScript than they are in Haskell (Haskell curries by default, where JS doesn't). The Applicative Functor interface could be made available in some other forms instead: -## Via liftA2 +### Via liftA2 Instead of a type like `.apply`'s: @@ -196,7 +198,7 @@ I didn't want to open the RFC with this, as Applicative Functors are instead usu I don't love the name `.with` either. For reference in Haskell `.with` is called `liftA2`. -## Via sequence +### Via sequence Promises are also valid Applicative Functors. While `.then` does have all the power of `.apply` (and then some), there is a particularly Applicative-y util for Promises: `Promise.all` (_handling of TypeScript tuples omitted for brevity_): @@ -218,8 +220,31 @@ However, there are a number of reasons this interface isn't that great: However, I figured mentioning `Promise` would be useful for precedence. +## Alternatives to this proposal + +### Wrapping `useContext` + +As already noted, a custom hook wrapping `useContext` can no longer be called conditionally like `use` can. +Additionally, wrapping `useContext` will always be subscribed to any change, where derived Contexts will stop repeat pushes. + +However, a custom hook wrapper has a lot of power as the code using it evolves over time. +It can be made to accept arguments, or at some point switch to actively procuring the data. + +### [useContextSelector](https://github.com/reactjs/rfcs/pull/119) + +At an API level, this proposal competes with `useContextSelector` the most. +In those terms, I think derived Contexts are the better first option: +- `useContextSelector` can be defined in terms of derived Contexts, both in user-land and in React itself +- Derived contexts greatly increase the expressiveness of Context as a tool of composition, not just for state management +- No ambiguity about identity ("do I need to useMemo the selector?"): users expect `.map` to change identity + +However, at the performance level, derived Contexts might be able to be made performant enough to satisfy `useContextSelector`'s use cases. +Then its more universal applicability could justify adding this as a solution for those cases. + # Adoption strategy +No existing APIs are modified so it is opt-in only. + A codemod for TypeScript users _could_ be made to convert existing explicit type annotations of `Context`'s to `WritableContext`. Beyond that, everything should Just Work. From ccb3f38ab9160cb2f8f746445489c36a35c8e0de Mon Sep 17 00:00:00 2001 From: Caleb Stimpson Date: Sat, 12 Jul 2025 12:43:58 -0600 Subject: [PATCH 09/15] Mention that ReadonlyContext is supertype of Context --- text/0000-context-applicative.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-context-applicative.md b/text/0000-context-applicative.md index 92104b2f..629d8fcf 100644 --- a/text/0000-context-applicative.md +++ b/text/0000-context-applicative.md @@ -8,7 +8,7 @@ Selectors for Context are an in-demand feature, but with a slightly different in ([.slice as proposed here is similar](https://github.com/reactjs/rfcs/pull/119#issuecomment-512529871])). Following from functional programming techniques, this RFC proposes adding: -- a `ReadonlyContext` type which is like `Context` but without `.Provider` +- a `ReadonlyContext` type which is a supertype of `Context` without `.Provider` and these methods to Context: - `.map`, of type: `(this: ReadonlyContext, fn: (data: T) => U) => ReadonlyContext` From 7ac2d6564ba77ee4931213034e07e4f7b785c988 Mon Sep 17 00:00:00 2001 From: Caleb Stimpson Date: Sat, 12 Jul 2025 12:55:50 -0600 Subject: [PATCH 10/15] Rephrasing and style improvements pt 3 --- text/0000-context-applicative.md | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/text/0000-context-applicative.md b/text/0000-context-applicative.md index 629d8fcf..fcbb1305 100644 --- a/text/0000-context-applicative.md +++ b/text/0000-context-applicative.md @@ -21,7 +21,7 @@ To be usable within renders, existing user-land techniques like WeakMap can be u # Basic example -```js +```jsx // User can set either of these for our API const ViewportReadonly = createContext(null); const ViewportWritable = createContext(null); @@ -48,7 +48,7 @@ function Library() { # Motivation -Motivation for selecting Contexts is already well-established by the popularity of [https://github.com/reactjs/rfcs/pull/119](RFC 119). +Motivation for selecting Contexts is already well-established by the popularity of [RFC 119](https://github.com/reactjs/rfcs/pull/119). Motivation for this approach is that these `.map` and `.apply` functions are drawn from the Functor and Applicative Functor (respectively) from Haskell. This style of approach is thoroughly treaded ground, being used with great success in Haskell for many years. @@ -63,7 +63,7 @@ Now we can create a DAG, which is so much more expressive than just a tree. While I believe this interface is more pleasant in both ergonomics, efficiency and theory than wrapping `useContext`, what I believe is particularly powerful about it is **its ability to change over time**. If some code depends on a Context, that Context can later be rewritten in terms of other Contexts. -**The consumption is isolated from the production**: the producers can change without necessitating the consumers read new Contexts. +**The consumption is isolated from the production**: the producers can change without necessitating the consumers to read new Contexts. As an added bonus over wrapping `useContext`, derived Contexts could be used conditionally by `use`. @@ -114,7 +114,7 @@ function SomeComponent() { The second is that in this snippet, `Child` will never be rerendered (as least by the `useContext`). When the map function returns the same as it did for the last push by `Object.is` semantics, the returned Context will not push an update to its subscribers. -```ts +```tsx const CountContext = createContext(0); const ZeroContext = CountContext.map(() => 0); @@ -164,7 +164,7 @@ However, this only affects TypeScript users, and the vast majority of usages won ## Implementation complexity -TODO: I cannot say how complicated it is, but I would think this to not be overly complicated. +TODO: I do not know how complex this is. # Alternatives @@ -188,7 +188,7 @@ a function called something like `.with` could be exposed: (this: ReadonlyContext, other: ReadonlyContext, combine: (a: T, b: U) => R): ReadonlyContext ``` -Note this is the same as `.map`'ing `other` by `combine` into `(data: T) => R`, and similarly `.apply` could be defined in terms of `.with`. +Note this is the same as `.map`'ing `other` by `combine` into `(data: T) => R`. Similarly `.apply` could be defined in terms of `.with`. **I recommend this approach**. I think it would be more natural for JavaScript users, and for the typical use of combining two Contexts, only requires creating the output Context, @@ -246,12 +246,11 @@ Then its more universal applicability could justify adding this as a solution fo No existing APIs are modified so it is opt-in only. A codemod for TypeScript users _could_ be made to convert existing explicit type annotations of `Context`'s to `WritableContext`. -Beyond that, everything should Just Work. # How we teach this -These methods could share one docs page, and beyond that would be out of the users way. -For the users who stumble on it while poking around: +These methods could share one docs page, and beyond that would be out of the user's way. +For the users who stumble upon it while poking around: - `.map` is already a familiar name from Array, and - they are completely described by their types (not preserving identity is their only non-pure semantic, and that has strong precedence from Array `.map`). @@ -262,4 +261,4 @@ Finding an intuitive name for `.with` would be trickiest part. - `Context` + `ReadonlyContext` vs `WritableContext` + `Context` - Implementation complexity and performance of derivation -- Could default displayName of derived Contexts from input Context displayName's and the function names. +- Could default displayName of derived Contexts from input Context displayName's and the function names From 1cd66ee4ecb2746f6a720c2e7d8e336784a48ef6 Mon Sep 17 00:00:00 2001 From: Caleb Stimpson Date: Sat, 12 Jul 2025 12:57:42 -0600 Subject: [PATCH 11/15] Rename proposal to Derived Contexts --- text/{0000-context-applicative.md => 0000-derived-contexts.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename text/{0000-context-applicative.md => 0000-derived-contexts.md} (100%) diff --git a/text/0000-context-applicative.md b/text/0000-derived-contexts.md similarity index 100% rename from text/0000-context-applicative.md rename to text/0000-derived-contexts.md From 84e3ea97a8839d568808b8d149dbc01c8cb1b45b Mon Sep 17 00:00:00 2001 From: Caleb Stimpson Date: Sat, 12 Jul 2025 13:01:23 -0600 Subject: [PATCH 12/15] Show .with style in Basic example --- text/0000-derived-contexts.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/text/0000-derived-contexts.md b/text/0000-derived-contexts.md index fcbb1305..e07e621c 100644 --- a/text/0000-derived-contexts.md +++ b/text/0000-derived-contexts.md @@ -37,8 +37,12 @@ function IntegrateLibrary({ viewport }) { // Now ViewportRead will grab either, with a preference for ViewportWritable's const ViewportRead = ViewportReadonly.apply(ViewportWritable.map((writableViewport) => (readableViewport) => { - return writableViewport ?? readabelViewport; + return writableViewport ?? readableViewport; })); +// or using .with: +const ViewportRead = ViewportWritable.with(ViewportReadonly, (writableViewport, readableViewport) => { + return writableViewport ?? readableViewport; +}); function Library() { // Library will pick up either const viewport = useContext(ViewportRead); From 3b20491da519bd382a5e4fb618f575c0fe5ec3c4 Mon Sep 17 00:00:00 2001 From: Caleb Stimpson Date: Sat, 12 Jul 2025 13:12:58 -0600 Subject: [PATCH 13/15] Typo --- text/0000-derived-contexts.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-derived-contexts.md b/text/0000-derived-contexts.md index e07e621c..6ff36328 100644 --- a/text/0000-derived-contexts.md +++ b/text/0000-derived-contexts.md @@ -14,7 +14,7 @@ and these methods to Context: - `.map`, of type: `(this: ReadonlyContext, fn: (data: T) => U) => ReadonlyContext` - `.apply`, of type: `(this: ReadonlyContext, fn: ReadonlyContext<(data: T) => U>) => ReadonlyContext` (_I actually recommend one of its alternatives_) -`.map` allows users to refine and select their Contexts, while `.apply` allows users to combine multiple Contexts together. This is convered in detail in Motivation. +`.map` allows users to refine and select their Contexts, while `.apply` allows users to combine multiple Contexts together. This is covered in detail in Motivation. Every call, without respect to the identity of the given argument (function / context of functions), returns a distinct Context. To be usable within renders, existing user-land techniques like WeakMap can be used to stabilize the identity of the returned Context to the identity of the given argument. From 057ccb8d3fad7ed72198924688c143a4e4ffe35b Mon Sep 17 00:00:00 2001 From: Caleb Stimpson Date: Sat, 12 Jul 2025 16:34:11 -0600 Subject: [PATCH 14/15] Rephrasing and style improvements pt 4 --- text/0000-derived-contexts.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/text/0000-derived-contexts.md b/text/0000-derived-contexts.md index 6ff36328..bdf615fd 100644 --- a/text/0000-derived-contexts.md +++ b/text/0000-derived-contexts.md @@ -99,7 +99,7 @@ declare function showAsJSX(data: U): ReactNode; declare const SomeContext: ReadonlyContext; ``` -this `SomeComponent`: +Then, this `SomeComponent`: ```ts const MappedContext = SomeContext.map(someMapping); function SomeComponent() { @@ -131,7 +131,7 @@ const child = ; function Parent() { const [count, setCount] = useState(0); return ( -
rerender(n => n + 1)}> +
setCount(n => n + 1)}> {child} @@ -162,7 +162,7 @@ I also believe the the surface area is satisfying small, with no extra hooks or Contexts derived from other Contexts cannot have `.Provider`'s. If derived Contexts had been around from day one of Context, I think distinguishing a `WritableContext` type would be more useful than distinguishing a `ReadonlyContext` (as not many places where the types can't be inferred would need to then provide a Context value). -I would not think a codemod to rewrite current `Context`'s to `WritableContext` would be worth it. +I would not think a codemod to rewrite current `Context`'s to `WritableContext`'s would be worth it. However, this only affects TypeScript users, and the vast majority of usages won't have explicit type annotations. @@ -195,7 +195,7 @@ a function called something like `.with` could be exposed: Note this is the same as `.map`'ing `other` by `combine` into `(data: T) => R`. Similarly `.apply` could be defined in terms of `.with`. **I recommend this approach**. -I think it would be more natural for JavaScript users, and for the typical use of combining two Contexts, only requires creating the output Context, +I think it would be more natural for JavaScript users, and considering the typical use of combining two Contexts, it only requires creating the output Context, where `.apply` requires creating the intermediate Context from `.map`'ing `other`. I didn't want to open the RFC with this, as Applicative Functors are instead usually introduced with `.apply`. @@ -219,8 +219,8 @@ This pattern is called `sequence` in Haskell. Exposing a function like: would give the same power to users as `.apply`. However, there are a number of reasons this interface isn't that great: -- combining more than two or three Contexts in not that common, for which the Array is needless overhead -- there is no Context namespace, so it would need to be exposed as a plain function from 'react' +- combining more than two or three Contexts is not that common, for which the Array is needless overhead +- there is no Context namespace, so it would need to be exposed as a plain function from 'react'. I don't know what'd be a nice name for it. However, I figured mentioning `Promise` would be useful for precedence. @@ -243,13 +243,13 @@ In those terms, I think derived Contexts are the better first option: - No ambiguity about identity ("do I need to useMemo the selector?"): users expect `.map` to change identity However, at the performance level, derived Contexts might be able to be made performant enough to satisfy `useContextSelector`'s use cases. -Then its more universal applicability could justify adding this as a solution for those cases. +Then the more universal applicability of derived Contexts could justify adding (it as) a solution for those cases. # Adoption strategy No existing APIs are modified so it is opt-in only. -A codemod for TypeScript users _could_ be made to convert existing explicit type annotations of `Context`'s to `WritableContext`. +A codemod for TypeScript users _could_ be made to convert existing explicit type annotations of `Context`'s to `WritableContext`'s. # How we teach this @@ -259,7 +259,7 @@ For the users who stumble upon it while poking around: - `.map` is already a familiar name from Array, and - they are completely described by their types (not preserving identity is their only non-pure semantic, and that has strong precedence from Array `.map`). -Finding an intuitive name for `.with` would be trickiest part. +Finding an intuitive name for `.with` would be the trickiest part. # Unresolved questions From 3672ffbf5d98877ae1237b8eec92cbf405b005ec Mon Sep 17 00:00:00 2001 From: Caleb Stimpson Date: Sun, 13 Jul 2025 12:39:56 -0600 Subject: [PATCH 15/15] Rephrasing and style improvements pt 5 --- text/0000-derived-contexts.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/text/0000-derived-contexts.md b/text/0000-derived-contexts.md index bdf615fd..e8455970 100644 --- a/text/0000-derived-contexts.md +++ b/text/0000-derived-contexts.md @@ -156,7 +156,7 @@ I believe it is worth the surface area as: - custom hooks wrapping `useContext` will not be allowed to be called conditionally with `use`. - anything wanting to preserve `use` semantics could not `useMemo` to avoid expensive recomputation, where `.map` allows that expensive computation to be lifted out of renders. -I also believe the the surface area is satisfying small, with no extra hooks or exports. +I also believe the surface area is satisfying small, with no extra hooks or exports. ## ReadonlyContext @@ -240,7 +240,7 @@ At an API level, this proposal competes with `useContextSelector` the most. In those terms, I think derived Contexts are the better first option: - `useContextSelector` can be defined in terms of derived Contexts, both in user-land and in React itself - Derived contexts greatly increase the expressiveness of Context as a tool of composition, not just for state management -- No ambiguity about identity ("do I need to useMemo the selector?"): users expect `.map` to change identity +- No questions about identity ("do I need to useMemo the selector?"): users expect `.map` to change identity However, at the performance level, derived Contexts might be able to be made performant enough to satisfy `useContextSelector`'s use cases. Then the more universal applicability of derived Contexts could justify adding (it as) a solution for those cases.