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
componentandhookto 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
useCallbackoruseMemo.
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:
effectis a syntax sugar foruseEffect, the semantics are the same.effectis 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
effectsyntax 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:
-
cleanupappears only immediately insideeffectblock. It cannot appear outside ofeffectblock or nested inside other blocks likeiforfor. -
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
serverUrlandroomIdin this example. -
cleanupdoes not necessarily need to be at the end of theeffectblock. It can appear anywhere inside theeffectblock.
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 changesuseEffect 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
awaitare executed immediately, just like regularuseEffect - statements after
awaitare wrapped in async IIFE cleanupcan capture variables beforeawait, but not afterawaitsince 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!
For people who are interested, I made an editor demo which implements the syntax: https://javascript-for-react.vercel.app/
