From 297c74b3da05613a45b18614aea0382d11863680 Mon Sep 17 00:00:00 2001 From: Ben Carp Date: Sat, 18 Jan 2020 20:46:19 +0200 Subject: [PATCH 1/5] Create adding-syntactic-features.md --- text/adding-syntactic-features.md | 278 ++++++++++++++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100644 text/adding-syntactic-features.md diff --git a/text/adding-syntactic-features.md b/text/adding-syntactic-features.md new file mode 100644 index 00000000..9d89307d --- /dev/null +++ b/text/adding-syntactic-features.md @@ -0,0 +1,278 @@ +- Start Date: 2020-01-18 +- RFC PR: (leave this empty) +- React Issue: (leave this empty) + +# Summary +Adding syntactic features to improve developer experience. + +# Motivation +React is well known (and rightly so) for enabling declarative UI programming. However, certain aspects of programming in modern React require technical, verbose, and in certain cases trivial code. Such is the case when trying to update a nested state value immutably, or when passing dependencies to hooks. + +JSX shows how we can significantly improve developer experience in React. +JSX has introduced an elegant syntax-compilation solution. +```jsx +child +``` +Compiles to: +```js +React.createElement("element", {prop:"value", children:"child"}) +``` +This solution allows us to enjoy both worlds - stay in JS, yet use dedicated syntax which makes our code easier to read and write. This RFC is about extending this (syntax-compilation) pattern to the basic hooks, and state management in React to significantly improve developer experience. User will be able to opt-in to easy to use compilation solutions, by using dedicated syntax. + +Hooks are ideal candidates: +1. Hooks come with non trivial constraints (Returned value depends on execution order, so they must be at the top level). With the use of a dedicated syntax, Hooks can be grasped and treated as different from a regular function call. +2. Optimizing is more difficult - Optimizing for performance requires knowledge and attention to implementation details, including: React.memo, useCallback, useMemo, and passing dependencies. Optimizing requires more code, and is somewhat more [bug prone](https://stackoverflow.com/questions/53070970/infinite-loop-in-useeffect). + +Another ideal candidate is state management, which comes with it's own necessary constraints. Most notably is that updating state should be done immutably. While this is essential in React, it could also be a burden on the developer, and is bug prone. This could also be elegantly solved with compilation. + +From a different perspective, React relies on immutability and memoization for performance, but both require technical verbose code. While immutability and memoization are not native to JS, we can provide React developers with an environment in which memoization and immutability feel native. + +# Basic example +The current example and syntax might be controversial but demonstrate the general proposal, and indicate a few possible directions. + +Below is a hypothetical "Complete and Share" app (a to do app, with two features - There are subtasks, and there is a social aspect - The user can view stats regarding the number of tasks his contacts have completed, and inform them accordingly.) + +```jsx +const CompleteAndShare = ({contactsCompletedStats, contacts}) => { + const [tasks, setTasks] = useState(initialTasks) + + + const toggleCompleted = useCallback( + (id) => setTasks( + tasks => tasks.map( + task => (task.id!=id)? task : {...task, completed: !task.completed} + ) + ), + [] + ) + + const editSubTask = useCallback( + (taskId, subTaskId, newText) => setTasks( + tasks => tasks.map( + (task, i) => (taskId!=i)? task : + {...task, subTasks: task.subTasks.map ( (subTask, j) => + (subTask.id ===j)? subTask : {...subTask, newText} + )} + ) + ), + [] + ) + + const completedTasks = useMemo( + ()=>tasks.reduce( + (accum, task) => accum+task.completed, + 0 + ), + [tasks] + ) + + const shareCompletedTasks = useCallback( + (contactId) => API.shareCompleted(contactId, completedTasks), + [completedTasks] + ) + + return ( + <> +

Complete and share

+ + + + + + + + ) +} + +export default React.memo(CompleteAndShare) +``` + +Below is the same functionality, relying on syntax-compilation to solve the challenges mentioned above. We shall use the following syntax for: +`S:` - Declaration of a state entity +`M:` - Declaration of a memoized entity +`#` - Shorthand attribute assignment to a child element. (similar to ES6 Object shorthand property names). + +```jsx +M: CompleteAndShare = ({contactsCompletedStats, contacts}) => { + s: tasks = initialTasks + + M: toggleCompleted = (id) => tasks[id].completed = !tasks[id].completed + + M: editSubTask = (taskId, subTaskId, newText) => tasks[taskId][subTaskId].text = newText + + M: completedTasks = tasks.reduce( + (accum, task) => accum+task.completed, + 0 + ) + + + M: shareCompletedTasks = (contactId) => API.shareCompleted(contactId, completedTasks) + + return ( + <> +

Complete and share

+ + + + + + + + ) +} + +export default CompleteAndShare +``` + +In terms of characters it is more than 40% shorter, but more important, it includes less implementation details, and is easier to write, read and understand. + +# Detailed design + +## Memoisation +In the example below: +```jsx +M: shareCompletedTasks = (contactId) => API.shareCompleted(contactId, completedTasks) +``` +the compiler needs to memoize `shareCompletedTasks`. `shareCompletedTasks` is a function and has one dependency (`completedTasks`). It can therefore convert to: + +```jsx +const shareCompletedTasks = useCallback( + (contactId) => API.shareCompleted(contactId, completedTasks), + [completedTasks] +) +``` + +However, using compilation enables new patterns: +1. The function that is passed to useCallback is recreated each time, even if an older version is used. Recreation could be avoided if the compiler moves the function outside the component body. + +```jsx +const memoizedCompletedTasks = generalMemoisingFunction( + completedTasks => contactId => API.shareCompleted(contactId, completedTasks) +) + +const TasksApp = { + ... + const shareCompletedTasks = memoizedCompletedTasks(completedTasks) + ... +} +``` +2. Currently the returned value depends on execution order. Consequently, inline callbacks can not be memoized, as the element might be conditionally rendered. The pattern suggested in section 1 does not rely on execution order so can also work for inline callbacks. +```jsx + API.shareCompleted(contactId, completedTasks) + } + contacts={contacts} + /> +``` + +In our new CompleteAndShare example all memoized values share the same syntax. Based on the memoized object, the compiler can wrap the code in React.memo, useCallback or useMemo. This syntax-compilation pattern hides memoization implementation details, and frees the developer from worrying about it. + +## State +In the example below: +```jsx +S: tasks = initialTasks + +M: editSubTask = (taskId, subTaskId, newText) => tasks[taskId][subTaskId].text = newText +``` +tasks is defined as a state entity. The compiler will convert the declaration to: +```jsx +const [tasks, setTasks] = useState(initialTasks) +``` + +The compiler recognizes that a state entity is being mutated/updated in editSubTask. It should first convert to: +```jsx +M: editSubTask = (taskId, subTaskId, newText) => setTasks( tasks => + tasks[taskId][subTaskId].text = newText +) +``` +All that is left is to convert mutating code to an immutable update. Immer has shown it to be possible in modern JS. One possible implementation is shown below: +```jsx +M: editSubTask = (taskId, subTaskId, newText) => setTasks( Immer.produce( + tasks => tasks[taskId][subTaskId].text = newText +)) +``` + +## Shorthand attribute assignment +(Related discussions and PRs: https://github.com/facebook/jsx/issues/23, https://github.com/facebook/jsx/pull/118, https://github.com/facebook/jsx/pull/121). + +Code reuse in React is achieved via component reuse. Moving from: +```jsx + +``` +to +```jsx + +``` +means that it is now easier to use FancyButton. Shorthand attribute assignment can make components more reusable (In the sense that it is easier to reuse the component than recreate it's functionality). + +Currently `` is equivalent to ``. Changing it to mean `` will be a major breaking change since this syntax is being used to specify modes of child elements. The community offered a few alternatives, but so far none has been accepted. One popular suggestion was to use ``, another was to use ``. The major criticism/reservation was that this syntax did not coherently fit current JSX or JS syntax. + +`#` can not be used in an identifier, and isn't an operator. Therefore it will not conflict with current use. + +# Drawbacks + +- Any change to the syntax of a language initially fragments the community to a certain degree. Early adopters might use the new syntax and initially not be understood by more traditional developers. +- Currently "JSX is an XML-like syntax extension to ECMAScript". It is used to template HTML in JS. While JSX is associated with React, it is independent to React and is used elsewhere. The current suggestions is beyond the scope of JSX, and will require distinct file extension. +- Is it a breaking change? Not necessarily. Typescript added new syntactic features that do not exist in Javascript. So did JSX. However, JS code is syntactically valid in JSX and Typescript. The challenge is to add syntactic features, yet for current JSX syntax to be valid in the new syntax. + +# Alternatives +- Suggested declarations use a different pattern than JS (`M:` compared to `const`, `let`, and `function`). The different pattern indicates that the suggested declarations are not exactly JS. An alternative would be to use declarations which are more aligned with JS declarations, such as `memo` and `state`. +- To signal shorthand attribute assignment, instead of `#`, consider @GnsP [suggestion](https://github.com/facebook/jsx/pull/121#issuecomment-528987940) for the `&` operator. + +## Babel Macros +- Instead of introducing new syntax, improve developer experience by leveraging Babel macros. We shall use the following macros: +`state` - state macro +`$` - general memoizing macro +`_` - shorthand JSX macro + +```jsx +import {state, $, _} from "react/macros" + +const CompleteAndShare = ({contactsCompletedStats, contacts}) => { + const tasks = state(initialTasks) + + const toggleCompleted = $( (id) => tasks[id].completed = !tasks[id].completed ) + + const editSubTask = $( (taskId, subTaskId, newText) => tasks[taskId][subTaskId].text = newText ) + + const completedTasks = $( tasks.reduce( + (accum, task) => accum+task.completed, + 0 + )) + + + const shareCompletedTasks = $( (contactId) => API.shareCompleted(contactId, completedTasks) ) + + return _( + <> +

Complete and share

+ + + + + + + + ) +}) + +export default $(CompleteAndShare) +``` + +# Adoption strategy + +More discussion, and a better vision of the API are required before considering adoption strategy. + +# How we teach this + +This RFC will actually allow an easier and smoother entry to React. A new developer will no longer need to master +- how to correctly update state in an immutable way. +- various memoization tools. + +Documentation could put less emphasis on the above, and instead: +- Present the new syntactic tools, and how to use them. +- Explain when it makes sense to memoize. + +# Unresolved questions +- How to pass state values and state setters to child components, and to functions. From a25a6902beccb51118ea995f21c555b4f4c949e6 Mon Sep 17 00:00:00 2001 From: Ben Carp Date: Sat, 18 Jan 2020 22:22:03 +0200 Subject: [PATCH 2/5] Update adding-syntactic-features.md fix macro alternative --- text/adding-syntactic-features.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/text/adding-syntactic-features.md b/text/adding-syntactic-features.md index 9d89307d..36bcdb33 100644 --- a/text/adding-syntactic-features.md +++ b/text/adding-syntactic-features.md @@ -224,10 +224,9 @@ Currently `` is equivalent to ` { const tasks = state(initialTasks) @@ -243,16 +242,16 @@ const CompleteAndShare = ({contactsCompletedStats, contacts}) => { const shareCompletedTasks = $( (contactId) => API.shareCompleted(contactId, completedTasks) ) - - return _( + + return ( <>

Complete and share

+ - - - - - + + + + ) }) From a2b57f91b1e0b10f164c0ff76fe40823ad49d119 Mon Sep 17 00:00:00 2001 From: Ben Carp Date: Tue, 21 Jan 2020 21:06:19 +0200 Subject: [PATCH 3/5] Adjustments to the Babel Macro alternative --- text/adding-syntactic-features.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/text/adding-syntactic-features.md b/text/adding-syntactic-features.md index 36bcdb33..6ed08670 100644 --- a/text/adding-syntactic-features.md +++ b/text/adding-syntactic-features.md @@ -215,34 +215,34 @@ Currently `` is equivalent to ` { - const tasks = state(initialTasks) + const [tasks, setTasks] = useStateM(initialTasks) - const toggleCompleted = $( (id) => tasks[id].completed = !tasks[id].completed ) + const toggleCompleted = $( (id) => setTasks(tasks[id].completed = !tasks[id].completed ) ) - const editSubTask = $( (taskId, subTaskId, newText) => tasks[taskId][subTaskId].text = newText ) + const editSubTask = $( (taskId, subTaskId, newText) => setTasks(tasks[taskId][subTaskId].text = newText )) const completedTasks = $( tasks.reduce( (accum, task) => accum+task.completed, 0 )) - const shareCompletedTasks = $( (contactId) => API.shareCompleted(contactId, completedTasks) ) - + return ( <>

Complete and share

From 92efe856d197bb98986e23d8c2316c61adfd2099 Mon Sep 17 00:00:00 2001 From: Ben Carp Date: Sat, 25 Jan 2020 18:44:50 +0200 Subject: [PATCH 4/5] Removing shortand attribute assignment to allow for a more focused discussion. --- text/adding-syntactic-features.md | 35 ++++++++----------------------- 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/text/adding-syntactic-features.md b/text/adding-syntactic-features.md index 6ed08670..dac31f26 100644 --- a/text/adding-syntactic-features.md +++ b/text/adding-syntactic-features.md @@ -3,7 +3,9 @@ - React Issue: (leave this empty) # Summary -Adding syntactic features to improve developer experience. +Adding compilation solutions to improve developer experience. The RFC considers the following: +- Adding new syntactic features +- As an alternative, leveraging Babble Macros. # Motivation React is well known (and rightly so) for enabling declarative UI programming. However, certain aspects of programming in modern React require technical, verbose, and in certain cases trivial code. Such is the case when trying to update a nested state value immutably, or when passing dependencies to hooks. @@ -90,7 +92,6 @@ export default React.memo(CompleteAndShare) Below is the same functionality, relying on syntax-compilation to solve the challenges mentioned above. We shall use the following syntax for: `S:` - Declaration of a state entity `M:` - Declaration of a memoized entity -`#` - Shorthand attribute assignment to a child element. (similar to ES6 Object shorthand property names). ```jsx M: CompleteAndShare = ({contactsCompletedStats, contacts}) => { @@ -111,12 +112,12 @@ M: CompleteAndShare = ({contactsCompletedStats, contacts}) => { return ( <>

Complete and share

+ - - - - - + + + + ) } @@ -124,7 +125,7 @@ M: CompleteAndShare = ({contactsCompletedStats, contacts}) => { export default CompleteAndShare ``` -In terms of characters it is more than 40% shorter, but more important, it includes less implementation details, and is easier to write, read and understand. +It is significantly shorter, but more important, it includes less implementation details, and is easier to write, read and understand. # Detailed design @@ -193,23 +194,6 @@ M: editSubTask = (taskId, subTaskId, newText) => setTasks( Immer.produce( )) ``` -## Shorthand attribute assignment -(Related discussions and PRs: https://github.com/facebook/jsx/issues/23, https://github.com/facebook/jsx/pull/118, https://github.com/facebook/jsx/pull/121). - -Code reuse in React is achieved via component reuse. Moving from: -```jsx - -``` -to -```jsx - -``` -means that it is now easier to use FancyButton. Shorthand attribute assignment can make components more reusable (In the sense that it is easier to reuse the component than recreate it's functionality). - -Currently `` is equivalent to ``. Changing it to mean `` will be a major breaking change since this syntax is being used to specify modes of child elements. The community offered a few alternatives, but so far none has been accepted. One popular suggestion was to use ``, another was to use ``. The major criticism/reservation was that this syntax did not coherently fit current JSX or JS syntax. - -`#` can not be used in an identifier, and isn't an operator. Therefore it will not conflict with current use. - # Drawbacks - Any change to the syntax of a language initially fragments the community to a certain degree. Early adopters might use the new syntax and initially not be understood by more traditional developers. @@ -219,7 +203,6 @@ Currently `` is equivalent to ` Date: Sat, 25 Jan 2020 18:55:40 +0200 Subject: [PATCH 5/5] Renaming RFC to address misunderstanding Current discussions refers only to adding syntactic features. But the essence of this RFC is improving developer experience with compilation solutions. --- ...ding-syntactic-features.md => adding-compilation-solutions.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename text/{adding-syntactic-features.md => adding-compilation-solutions.md} (100%) diff --git a/text/adding-syntactic-features.md b/text/adding-compilation-solutions.md similarity index 100% rename from text/adding-syntactic-features.md rename to text/adding-compilation-solutions.md