3.4 KB103 lines
Blame
1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8const containerMethods = new Set(['get', 'has']);
9
10/**
11 * Example:
12 *
13 * function MaybeHighlight(props: {id: string}) {
14 * const set = useAtomValue(selectedSet);
15 * const selected = set.has(props.id)
16 * return selected ? <Highlight /> : null;
17 * }
18 *
19 * will trigger re-render of all MaybeHigh once the atom has any small
20 * changes. To only re-render changed items, use `atomFamilyWeak`:
21 *
22 * const selectedById = atomFamilyWeak((id: string) => {
23 * return atom(get => get(selectedsSet).has(id));
24 * });
25 * function MaybeHighlight(props: {id: string}) {
26 * const selected = useAtomValue(selectedById(props.id));
27 * ...
28 * }
29 *
30 * Alternatively, calculate a memo-ed atom on demand:
31 *
32 * function MaybeHighlight({id}: {id: string}) {
33 * const selectedAtom = useMemo(() => atom(get => get(selectedsSet).has(id)), [id]);
34 * const selected = useAtomValue(selectedAtom);
35 * ...
36 * }
37 *
38 * The `atomFamilyWeak` might keep some extra states alive to satisfy other
39 * use-cases. The memo-ed atom approach has no memory leak and might be
40 * preferred if there are only 1 component that needs this derived atom state.
41 */
42module.exports = {
43 meta: {
44 type: 'problem',
45 docs: {
46 description: 'Suggest alternatives for container-get-key patterns to avoid re-render.',
47 },
48 },
49 create(context) {
50 return {
51 VariableDeclarator(node) {
52 if (node.init?.type === 'CallExpression' && node.init.callee.name === 'useAtomValue') {
53 analyzeUseRecoilValue(node, context);
54 }
55 },
56 };
57 },
58};
59
60function analyzeUseRecoilValue(node, context) {
61 const varName = node.id.name;
62 // Analyze references to this variable.
63 const sourceCode = context.sourceCode ?? context.getSourceCode();
64 const scope = sourceCode.getScope?.(node) ?? context.getScope();
65 // The container method being used: "get" or "has".
66 let method = null;
67 // Find references of this variable.
68 const references = scope.variables.find(({name}) => name === varName)?.references ?? [];
69 // Check the references. The first one is the declaration and should be skipped.
70 for (const reference of references.slice(1)) {
71 // Inside a loop for potentially legit usecase. Allow.
72 const innerScope = reference.from;
73 if (innerScope?.block.type === 'ArrowFunctionExpression') {
74 return;
75 }
76 let nextNode = sourceCode.getTokenAfter(reference.identifier);
77 // Container methods like ".get" or ".has"?
78 let currentMethod = null;
79 if (nextNode?.type === 'Punctuator' && (nextNode.value === '.' || nextNode.value === '?.')) {
80 nextNode = sourceCode.getTokenAfter(nextNode);
81 if (nextNode?.type === 'Identifier' && containerMethods.has(nextNode.value)) {
82 currentMethod = method = nextNode.value;
83 }
84 }
85 // Called other methods, or have other use-cases. Allow.
86 if (currentMethod === null) {
87 return;
88 }
89 }
90 if (method !== null) {
91 context.report({
92 node,
93 message:
94 'Atom value `{{ varName }}` seems to be only used for `{{ method }}`. Consider moving `{{ method }}` to a `atomFamilyWeak` or use `{{ useMethod }}` to avoid re-render.',
95 data: {
96 varName,
97 method,
98 useMethod: method === 'get' ? 'useAtomGet' : 'useAtomHas',
99 },
100 });
101 }
102}
103