This syntax extension is a mere fantasy of future React Script syntax. It is not a RFC, proposal or suggestion to React team or community. It is just a fun idea to imagine how React could be in the future.
Read this like a fan-fiction, for fun. :)
There are two recent new features in React community that are introducing new extensions to JavaScript, both syntax and semantics:
-
Flow introduces two new keywords
component
andhook
to differentiate regular functions from React components and hooks. -
React Compiler introduces new auto-memoization semantics to React components, which allows React to automatically memoize components without using
useCallback
oruseMemo
.
The compiler now can automatically detect the dependencies and mutations inside components and memoize them automatically. This is a huge improvement in DX for React developers so we don't need to annotate useMemo
/useCallback
dependencies manually. Can we extend this idea further to useEffect
? So that we don't need to add manual deps array.
Can we make it even better by introducing a new syntax to make useEffect
smarter?
The synergy between Syntax and Compiler naturally gives rise to the idea of a new syntax useEffect
.
What if we can have a new keyword in Flow syntax: effect
? This keyword is a new syntax sugar for useEffect
, but it can automatically detect the dependencies of the effect callback function.
For example:
component Map({ zoomLevel }) {
const containerRef = useRef(null)
const mapRef = useRef(null)
effect {
if (mapRef.current === null) {
mapRef.current = new MapWidget(containerRef.current);
}
const map = mapRef.current;
map.setZoom(zoomLevel);
} // no deps array! zoomLevel is automatically detected as a dependency
// ....
}
is equivalent to
export default function Map({ zoomLevel }) {
const containerRef = useRef(null);
const mapRef = useRef(null);
useEffect(() => {
if (mapRef.current === null) {
mapRef.current = new MapWidget(containerRef.current);
}
const map = mapRef.current;
map.setZoom(zoomLevel);
}, [zoomLevel]); // zoomLevel automatically added here
// ....
}
effect
accepts a block statement { ... }
as its body, and it will be translated to useEffect
with automatically detected dependency array.
There are several principles behind this new syntax:
effect
is a syntax sugar foruseEffect
, the semantics are the same.effect
is a DX improvement since users will not need annotate the dep array most of the time.- users can optionally annotate the dep array manually if they want to handle edge cases
effect
syntax can be further extended to support more features in the future, such as async effects
effect
can also have a cleanup
block inside it, which is equivalent to useEffect
with cleanup function:
hook useChatRoom(roomId) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
effect {
const connection = createConnection(serverUrl, roomId);
connection.connect();
cleanup {
connection.disconnect();
}
}
}
is equivalent to
function useChatRoome(roomId) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
}
cleanup
's design:
-
cleanup
appears only immediately insideeffect
block. It cannot appear outside ofeffect
block or nested inside other blocks likeif
orfor
. -
It will only run when the effect is cleaned up.
-
It can also capture the variables from the effect scope and outer component scope, such as
serverUrl
androomId
in this example. -
cleanup
does not necessarily need to be at the end of theeffect
block. It can appear anywhere inside theeffect
block.
cleanup
is like defer
in Go, but we will see why it is useful in async effect.
effect
can also optionally accept a deps array, just like useEffect
:
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
effect([serverUrl, roomId]) {
const connection = createConnection(serverUrl, roomId);
connection.connect();
}
This is useful when you want to skip some dependencies from auto-detection for edge cases.
Currently intentionally skipping dependency requires eslint-disable comments. The explicit effect
syntax can convey the intention more clearly without needing to disable linter.
Here are some common use cases:
effect { ... } // auto detect deps
effect([]) { ... } // run only on mounting
effect() { ... } // run every time when compontn re-renders
effect([serverUrl]) { ... } // run only when serverUrl changes
useEffect
does not support async function as argument because the return type must be a cleanup function instead of a promise. Using async
function inside useEffect
will not work as expected because React cannot call the cleanup function before the promise is resolved.
effect
can be extended to support async function as its body, since cleanup
can be compiled out of the async function and returned earlier.
component Page() {
const [person, setPerson] = useState('Alice');
const [bio, setBio] = useState(null);
async effect {
let ignore = false;
cleanup { ignore = true; }
setBio(null);
const result = await fetchBio(person);
if (!ignore) {
setBio(result);
}
}
// ...
}
The code is equivalent to:
function Page() {
const [person, setPerson] = useState('Alice');
const [bio, setBio] = useState(null);
useEffect(() => {
// statement before first await is executed immediately
let ignore = false;
setBio(null);
// statements after await are wrapped in async IIFE
(async function () {
const result = await fetchBio(person);
if (!ignore) {
setBio(result);
}
})();
// return cleanup function
return () => { ignore = true; }
}, [person]);
}
The rule of async effect
:
- statements before first
await
are executed immediately, just like regularuseEffect
- statements after
await
are wrapped in async IIFE cleanup
can capture variables beforeawait
, but not afterawait
since they are in a different scope. Compiler can emit an error if this rule is violated.
This can be roughly inspired by Vue's watchEffect which supports async function.
I hope you enjoyed this fantasy and it can be a fun idea to think about the ergonomics of React in the future!
Effect syntax would be cool! Your syntax looks pretty spot on.
For the
useState
syntax, i like the idea but the thing i have a hard time with is if you created a custom hook which creates a state things get awkward. e.g.I think ideally we would also allow custom hooks to expose new "state" like you can do today by just matching the
useState
return API. Any thoughts on how that syntax could look?