Skip to content

Commit 4de8e83

Browse files
Prevent excessive rerenders with useList
Use the `.once`’s `val()` to determine the number of children, then batch that many calls to the `child_added` handler into one `value` dispatch, resulting in one additional render instead of one per child. Fix shadowing of `query` variable, use `ref` instead. Fix the `ref.off` calls to pass the right function to unsubscribe. Fix dependencies on useEffect. Fixes #74
1 parent dfdee4a commit 4de8e83

File tree

2 files changed

+110
-47
lines changed

2 files changed

+110
-47
lines changed

database/helpers/useListReducer.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ type RemoveAction = {
3333
snapshot: database.DataSnapshot | null;
3434
};
3535
type ResetAction = { type: 'reset' };
36-
type ValueAction = { type: 'value' };
36+
type ValueAction = { type: 'value'; snapshots: database.DataSnapshot[] | null };
3737
type ReducerAction =
3838
| AddAction
3939
| ChangeAction
@@ -110,6 +110,7 @@ const listReducer = (
110110
...state,
111111
error: undefined,
112112
loading: false,
113+
value: setValue(action.snapshots),
113114
};
114115
case 'empty':
115116
return {
@@ -125,6 +126,30 @@ const listReducer = (
125126
}
126127
};
127128

129+
const setValue = (snapshots: database.DataSnapshot[] | null): KeyValueState => {
130+
if (!snapshots) {
131+
return {
132+
keys: [],
133+
values: [],
134+
};
135+
}
136+
137+
const keys: string[] = [];
138+
const values: database.DataSnapshot[] = [];
139+
snapshots.forEach((snapshot) => {
140+
if (!snapshot.key) {
141+
return;
142+
}
143+
keys.push(snapshot.key);
144+
values.push(snapshot);
145+
});
146+
147+
return {
148+
keys,
149+
values,
150+
};
151+
};
152+
128153
const addChild = (
129154
currentState: KeyValueState,
130155
snapshot: database.DataSnapshot,

database/useList.ts

Lines changed: 84 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -11,66 +11,104 @@ export type ListValsHook<T> = LoadingHook<T[], FirebaseError>;
1111
export const useList = (query?: database.Query | null): ListHook => {
1212
const [state, dispatch] = useListReducer();
1313

14-
const ref = useIsEqualRef(query, () => dispatch({ type: 'reset' }));
15-
16-
const onChildAdded = (
17-
snapshot: database.DataSnapshot | null,
18-
previousKey?: string | null
19-
) => {
20-
dispatch({ type: 'add', previousKey, snapshot });
21-
};
22-
23-
const onChildChanged = (snapshot: database.DataSnapshot | null) => {
24-
dispatch({ type: 'change', snapshot });
25-
};
26-
27-
const onChildMoved = (
28-
snapshot: database.DataSnapshot | null,
29-
previousKey?: string | null
30-
) => {
31-
dispatch({ type: 'move', previousKey, snapshot });
32-
};
33-
34-
const onChildRemoved = (snapshot: database.DataSnapshot | null) => {
35-
dispatch({ type: 'remove', snapshot });
36-
};
37-
38-
const onError = (error: FirebaseError) => {
39-
dispatch({ type: 'error', error });
40-
};
41-
42-
const onValue = () => {
43-
dispatch({ type: 'value' });
44-
};
14+
const queryRef = useIsEqualRef(query, () => dispatch({ type: 'reset' }));
4515

4616
useEffect(() => {
47-
const query: database.Query | null | undefined = ref.current;
48-
if (!query) {
17+
const ref: database.Query | null | undefined = queryRef.current;
18+
if (!ref) {
4919
dispatch({ type: 'empty' });
5020
return;
5121
}
52-
// This is here to indicate that all the data has been successfully received
53-
query.once('value', onValue, onError);
54-
query.on('child_added', onChildAdded, onError);
55-
query.on('child_changed', onChildChanged, onError);
56-
query.on('child_moved', onChildMoved, onError);
57-
query.on('child_removed', onChildRemoved, onError);
22+
23+
const onChildAdded = (
24+
snapshot: database.DataSnapshot | null,
25+
previousKey?: string | null
26+
) => {
27+
dispatch({ type: 'add', previousKey, snapshot });
28+
};
29+
30+
const onChildChanged = (snapshot: database.DataSnapshot | null) => {
31+
dispatch({ type: 'change', snapshot });
32+
};
33+
34+
const onChildMoved = (
35+
snapshot: database.DataSnapshot | null,
36+
previousKey?: string | null
37+
) => {
38+
dispatch({ type: 'move', previousKey, snapshot });
39+
};
40+
41+
const onChildRemoved = (snapshot: database.DataSnapshot | null) => {
42+
dispatch({ type: 'remove', snapshot });
43+
};
44+
45+
const onError = (error: FirebaseError) => {
46+
dispatch({ type: 'error', error });
47+
};
48+
49+
const onValue = (snapshots: database.DataSnapshot[] | null) => {
50+
dispatch({ type: 'value', snapshots });
51+
};
52+
53+
let childAddedHandler: ReturnType<typeof ref.on> | undefined;
54+
const children: database.DataSnapshot[] = [];
55+
const onInitialLoad = (snapshot: database.DataSnapshot) => {
56+
let childrenToProcess = Object.keys(snapshot.val()).length;
57+
58+
const onChildAddedWithoutInitialLoad = (
59+
addedChild: database.DataSnapshot,
60+
previousKey?: string
61+
) => {
62+
// process the first batch of children all at once
63+
if (childrenToProcess > 0) {
64+
childrenToProcess--;
65+
children.push(addedChild);
66+
67+
if (childrenToProcess === 0) {
68+
onValue(children);
69+
}
70+
71+
return;
72+
}
73+
74+
onChildAdded(snapshot, previousKey);
75+
};
76+
77+
childAddedHandler = ref.on(
78+
'child_added',
79+
onChildAddedWithoutInitialLoad,
80+
onError
81+
);
82+
};
83+
84+
ref.once('value', onInitialLoad, onError);
85+
const childChangedHandler = ref.on(
86+
'child_changed',
87+
onChildChanged,
88+
onError
89+
);
90+
const childMovedHandler = ref.on('child_moved', onChildMoved, onError);
91+
const childRemovedHandler = ref.on(
92+
'child_removed',
93+
onChildRemoved,
94+
onError
95+
);
5896

5997
return () => {
60-
query.off('child_added', onChildAdded);
61-
query.off('child_changed', onChildChanged);
62-
query.off('child_moved', onChildMoved);
63-
query.off('child_removed', onChildRemoved);
98+
ref.off('child_added', childAddedHandler);
99+
ref.off('child_changed', childChangedHandler);
100+
ref.off('child_moved', childMovedHandler);
101+
ref.off('child_removed', childRemovedHandler);
64102
};
65-
}, [ref.current]);
103+
}, [dispatch, queryRef]);
66104

67105
return [state.value.values, state.loading, state.error];
68106
};
69107

70108
export const useListKeys = (query?: database.Query | null): ListKeysHook => {
71109
const [value, loading, error] = useList(query);
72110
return [
73-
value ? value.map(snapshot => snapshot.key as string) : undefined,
111+
value ? value.map((snapshot) => snapshot.key as string) : undefined,
74112
loading,
75113
error,
76114
];
@@ -86,7 +124,7 @@ export const useListVals = <T>(
86124
const values = useMemo(
87125
() =>
88126
snapshots
89-
? snapshots.map(snapshot =>
127+
? snapshots.map((snapshot) =>
90128
snapshotToData(snapshot, options ? options.keyField : undefined)
91129
)
92130
: undefined,

0 commit comments

Comments
 (0)