forked from Sefaria/Sefaria-Mobile
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Hooks.js
140 lines (121 loc) · 5.24 KB
/
Hooks.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
'use strict';
import React, { useState, useEffect, useMemo, useCallback, useRef, useContext } from 'react';
import { GlobalStateContext, DispatchContext, STATE_ACTIONS, getTheme } from './StateManager';
export const useAsyncVariable = (initIsLoaded, loadVariable, onLoad) => {
/*
Loads a variable asynchronously and returns status of load
Useful for determining when a variable from the Sefaria object is available, e.g. Sefaria.calendar
*/
const [isLoaded, setIsLoaded] = useState(initIsLoaded);
const loadWrapper = useCallback(() => {
if (!isLoaded) {
return loadVariable();
}
return Promise.resolve();
}, [isLoaded, loadVariable]);
const onLoadWrapper = useCallback((data) => {
setIsLoaded(true);
if (onLoad) { onLoad(data); }
}, [onLoad]);
useAsync(loadWrapper, onLoadWrapper);
return isLoaded;
};
export const useAsync = (asyncFn, onSuccess) => {
useEffect(() => {
let isMounted = true;
asyncFn().then(data => {
if (isMounted) onSuccess(data);
});
return () => { isMounted = false };
}, [asyncFn, onSuccess]);
};
export function usePaginatedLoad(fetchDataByPage, setter, identityElement, numPages, resetValue=false) {
/*
See `useIncrementalLoad` docs
*/
const [page, setPage] = useState(0);
const [isCanceled, setCanceled] = useState({}); // dict {idElem: Bool}
const [valueQueue, setValueQueue] = useState(null);
const [finishedLoading, setFinishedLoading] = useState(false);
// When identityElement changes:
// Set current identityElement to not canceled
// Sets previous identityElement to canceled.
// Removes old items by calling setter(false);
// Resets page to 0
useEffect(() => {
setCanceled(d => { d[identityElement] = false; return Object.assign({}, d);});
return () => {
setCanceled(d => { d[identityElement] = true; return Object.assign({}, d);});
setter(resetValue);
setPage(0);
setFinishedLoading(false);
}}, [identityElement]);
const fetchPage = useCallback(() => fetchDataByPage(page), [page, fetchDataByPage]);
// make sure value setting callback and page procession get short circuited when id_elem has been canceled
// clear value queue on success
const setResult = useCallback((id_elem, val) => {
if (isCanceled[id_elem]) { setValueQueue(null); setFinishedLoading(true); return; }
setter(val);
setValueQueue(null);
if (page === numPages - 1 || numPages === 0) { setFinishedLoading(true); return; }
setPage(prevPage => prevPage + 1);
}, [isCanceled, setter, numPages, page, identityElement]);
// Make sure that current value is processed with latest setResult function
// if this is called from within the fetchPage effect, it will have stale canceled data
useEffect(() => {
if(valueQueue) {
setResult(...valueQueue);
}
}, [valueQueue, setResult]);
// Put value returned and originating identity element into value queue
useEffect(() => {
fetchPage()
.then((val, err) => setValueQueue([identityElement, val])).catch(error => {
if (error.error !== 'input not array') { throw error; }
});
}, [fetchPage]);
return finishedLoading;
}
export function useIncrementalLoad(fetchData, input, pageSize, setter, identityElement, resetValue=false) {
/*
Loads all items in `input` in `pageSize` chunks.
Each input chunk is passed to `fetchData`
fetchData: (data) => Promise(). Takes subarray from `input` and returns promise.
input: array of input data for `fetchData`
pageSize: int, chunk size
setter: (data) => null. Sets paginated data on component. setter(false) clears data.
identityElement: a string identifying a invocation of this effect. When it changes, pagination and processing will restart. Old calls in processes will be dropped on landing.
resetValue: value to pass to `setter` to indicate that it should forget previous values and reset.
*/
// When input changes, creates function to fetch data by page, computes number of pages
const [fetchDataByPage, numPages] = useMemo(() => {
const fetchDataByPage = (page) => {
if (!input) { return Promise.reject({error: "input not array", input}); }
const pagedInput = input.slice(page*pageSize, (page+1)*pageSize);
return fetchData(pagedInput);
};
const numPages = Math.ceil(input.length/pageSize);
return [fetchDataByPage, numPages];
}, [input]);
return usePaginatedLoad(fetchDataByPage, setter, identityElement, numPages, resetValue);
}
export function useGlobalState() {
// exposes global state context along with menu_language and theme which are derived from state
const state = useContext(GlobalStateContext);
const { interfaceLanguage, textLanguage, themeStr } = state;
const menuLanguage = Sefaria.util.get_menu_language(interfaceLanguage, textLanguage);
const theme = getTheme(themeStr);
return {
...state,
menuLanguage,
theme,
};
}
export function useRtlFlexDir(lang, dir='row', reverse) {
// return proper flexDirection depending on if lang is RTL or not
// reverse is boolean
// dir is either 'column' or 'row'
const isRTL = lang === 'hebrew';
const langReverse = isRTL ^ reverse; // rare situation where XOR makes sense
return `${dir}${langReverse ? '-reverse' : ''}`;
}