Will React Compiler (from React 19) affect Zustand in any way? #2562
-
|
The announcement of React compiler with the upcoming React 19 is generating a lot of discussion, due to how it will eliminate the need for manual memoization. I'm trying it myself, and I'm very impressed with the results. Out of the box optimization is not something we're used to in React land. So now I'm wondering, @dai-shi: will this React Compiler affect Zustand in some way, either its external API or its internal implementation? |
Beta Was this translation helpful? Give feedback.
Replies: 4 comments 13 replies
-
|
I don't think so. It should just work fine. |
Beta Was this translation helpful? Give feedback.
-
|
I'm having the same problem installing Next 19. For a minimal project just instal latest version. |
Beta Was this translation helpful? Give feedback.
-
|
It looks like auto-generating the selectors as described in the docs (https://zustand.docs.pmnd.rs/guides/auto-generating-selectors) is causing some issues with React compiler. See https://stackblitz.com/edit/vitejs-vite-vemxbafj?file=src%2FApp.tsx Using For some reason when I was debugging this yesterday I was getting a different error: Notice that these errors are not happening on the line that uses the generator, but on the next line that is calling a hook. I'll probably just switch to not using the autogenerated selectors, unless someone has a workaround? |
Beta Was this translation helpful? Give feedback.
-
|
I was having issues with direct destructuring a la // BREAKS (worked without react compiler)
const { foo, bar } = useMyStore()
// OK
const myStore = useMyStore(state => state)
const { foo, bar } = myStore;
// OK + more efficient
import { useShallow } from 'zustand/react/shallow';
...
const { foo, bar } =
useShoppingCartStore(
useShallow((state) => ({
foo: state.foo,
bar: state.bar,
})),
);Added a custom lint rule to resolve module.exports = {
create(context) {
return {
VariableDeclarator(node) {
// Check if we have a destructuring pattern on the left side
if (node.id.type !== 'ObjectPattern') {
return;
}
// Check if the right side is a call expression
if (!node.init || node.init.type !== 'CallExpression') {
return;
}
const callee = node.init.callee;
// Check if it's calling a function that matches useXxxStore pattern
if (callee.type !== 'Identifier' || !callee.name.match(/^use\w+Store$/)) {
return;
}
// Check if the hook is called without arguments (no selector)
// OR if it's not using useShallow
// This is the problematic pattern with React Compiler
const hasNoArgs = node.init.arguments.length === 0;
const hasUseShallow =
node.init.arguments.length > 0 &&
node.init.arguments[0].type === 'CallExpression' &&
node.init.arguments[0].callee.type === 'Identifier' &&
node.init.arguments[0].callee.name === 'useShallow';
if (hasNoArgs || (node.init.arguments.length > 0 && !hasUseShallow)) {
context.report({
data: {
storeName: callee.name,
},
fix(fixer) {
const sourceCode = context.getSourceCode();
const destructurePattern = node.id;
const storeHookName = callee.name;
// Extract the property names from the destructuring pattern
const properties = destructurePattern.properties
.map((prop) => {
if (prop.type === 'Property' && prop.key.type === 'Identifier') {
return prop.key.name;
}
return null;
})
.filter(Boolean);
// Build the useShallow selector
const selectorObject = properties
.map((propName) => `${propName}: state.${propName}`)
.join(',\n ');
const destructureText = sourceCode.getText(destructurePattern);
// Replace the entire variable declaration
const declaration = node.parent;
const declarationText = sourceCode.getText(declaration);
const declarationKeyword = declarationText.split(' ')[0]; // 'const', 'let', or 'var'
const fixes = [
fixer.replaceText(
declaration,
`${declarationKeyword} ${destructureText} = ${storeHookName}(\n useShallow((state) => ({\n ${selectorObject},\n }))\n);`,
),
];
// Check if useShallow is already imported
const programNode = sourceCode.ast;
const hasUseShallowImport = programNode.body.some(
(statement) =>
statement.type === 'ImportDeclaration' &&
statement.source.value === 'zustand/react/shallow' &&
statement.specifiers.some(
(spec) => spec.type === 'ImportSpecifier' && spec.imported.name === 'useShallow',
),
);
// Add import if not already present
if (!hasUseShallowImport) {
// Find the last import statement before any @ imports
let lastNonLocalImport = null;
for (const statement of programNode.body) {
if (statement.type === 'ImportDeclaration') {
if (!statement.source.value.startsWith('@')) {
lastNonLocalImport = statement;
}
} else if (lastNonLocalImport) {
// We've passed all imports
break;
}
}
if (lastNonLocalImport) {
fixes.push(
fixer.insertTextAfter(
lastNonLocalImport,
"\nimport { useShallow } from 'zustand/react/shallow';",
),
);
}
}
return fixes;
},
messageId: 'no-direct-zustand-destructure',
node,
});
}
},
};
},
meta: {
docs: {
category: 'Best Practices',
description:
'Require useShallow when destructuring from Zustand store hooks (React Compiler compatibility)',
recommended: true,
},
fixable: 'code',
messages: {
'no-direct-zustand-destructure':
"Use useShallow when destructuring from {{storeName}}. Direct destructuring breaks reactivity with React Compiler. Use: import { useShallow } from 'zustand/react/shallow'; then {{storeName}}(useShallow((state) => ({ ... })))",
},
},
rule: 'no-direct-zustand-destructure',
}; |
Beta Was this translation helpful? Give feedback.
I don't think so. It should just work fine.