addons/eslint-rules/jotai-maybe-use-family.jsblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318const containerMethods = new Set(['get', 'has']);
b69ab319
b69ab3110/**
b69ab3111 * Example:
b69ab3112 *
b69ab3113 * function MaybeHighlight(props: {id: string}) {
b69ab3114 * const set = useAtomValue(selectedSet);
b69ab3115 * const selected = set.has(props.id)
b69ab3116 * return selected ? <Highlight /> : null;
b69ab3117 * }
b69ab3118 *
b69ab3119 * will trigger re-render of all MaybeHigh once the atom has any small
b69ab3120 * changes. To only re-render changed items, use `atomFamilyWeak`:
b69ab3121 *
b69ab3122 * const selectedById = atomFamilyWeak((id: string) => {
b69ab3123 * return atom(get => get(selectedsSet).has(id));
b69ab3124 * });
b69ab3125 * function MaybeHighlight(props: {id: string}) {
b69ab3126 * const selected = useAtomValue(selectedById(props.id));
b69ab3127 * ...
b69ab3128 * }
b69ab3129 *
b69ab3130 * Alternatively, calculate a memo-ed atom on demand:
b69ab3131 *
b69ab3132 * function MaybeHighlight({id}: {id: string}) {
b69ab3133 * const selectedAtom = useMemo(() => atom(get => get(selectedsSet).has(id)), [id]);
b69ab3134 * const selected = useAtomValue(selectedAtom);
b69ab3135 * ...
b69ab3136 * }
b69ab3137 *
b69ab3138 * The `atomFamilyWeak` might keep some extra states alive to satisfy other
b69ab3139 * use-cases. The memo-ed atom approach has no memory leak and might be
b69ab3140 * preferred if there are only 1 component that needs this derived atom state.
b69ab3141 */
b69ab3142module.exports = {
b69ab3143 meta: {
b69ab3144 type: 'problem',
b69ab3145 docs: {
b69ab3146 description: 'Suggest alternatives for container-get-key patterns to avoid re-render.',
b69ab3147 },
b69ab3148 },
b69ab3149 create(context) {
b69ab3150 return {
b69ab3151 VariableDeclarator(node) {
b69ab3152 if (node.init?.type === 'CallExpression' && node.init.callee.name === 'useAtomValue') {
b69ab3153 analyzeUseRecoilValue(node, context);
b69ab3154 }
b69ab3155 },
b69ab3156 };
b69ab3157 },
b69ab3158};
b69ab3159
b69ab3160function analyzeUseRecoilValue(node, context) {
b69ab3161 const varName = node.id.name;
b69ab3162 // Analyze references to this variable.
b69ab3163 const sourceCode = context.sourceCode ?? context.getSourceCode();
b69ab3164 const scope = sourceCode.getScope?.(node) ?? context.getScope();
b69ab3165 // The container method being used: "get" or "has".
b69ab3166 let method = null;
b69ab3167 // Find references of this variable.
b69ab3168 const references = scope.variables.find(({name}) => name === varName)?.references ?? [];
b69ab3169 // Check the references. The first one is the declaration and should be skipped.
b69ab3170 for (const reference of references.slice(1)) {
b69ab3171 // Inside a loop for potentially legit usecase. Allow.
b69ab3172 const innerScope = reference.from;
b69ab3173 if (innerScope?.block.type === 'ArrowFunctionExpression') {
b69ab3174 return;
b69ab3175 }
b69ab3176 let nextNode = sourceCode.getTokenAfter(reference.identifier);
b69ab3177 // Container methods like ".get" or ".has"?
b69ab3178 let currentMethod = null;
b69ab3179 if (nextNode?.type === 'Punctuator' && (nextNode.value === '.' || nextNode.value === '?.')) {
b69ab3180 nextNode = sourceCode.getTokenAfter(nextNode);
b69ab3181 if (nextNode?.type === 'Identifier' && containerMethods.has(nextNode.value)) {
b69ab3182 currentMethod = method = nextNode.value;
b69ab3183 }
b69ab3184 }
b69ab3185 // Called other methods, or have other use-cases. Allow.
b69ab3186 if (currentMethod === null) {
b69ab3187 return;
b69ab3188 }
b69ab3189 }
b69ab3190 if (method !== null) {
b69ab3191 context.report({
b69ab3192 node,
b69ab3193 message:
b69ab3194 'Atom value `{{ varName }}` seems to be only used for `{{ method }}`. Consider moving `{{ method }}` to a `atomFamilyWeak` or use `{{ useMethod }}` to avoid re-render.',
b69ab3195 data: {
b69ab3196 varName,
b69ab3197 method,
b69ab3198 useMethod: method === 'get' ? 'useAtomGet' : 'useAtomHas',
b69ab3199 },
b69ab31100 });
b69ab31101 }
b69ab31102}