From 369734ab1860e36e23afc7595bde24fc0143ee15 Mon Sep 17 00:00:00 2001 From: "a.bouhuolia" Date: Wed, 6 Oct 2021 17:47:52 +0200 Subject: [PATCH] BIG-126: async localization loaded data failed to be injected to application. --- src/components/AppIntlLoader.js | 20 ++--- src/components/Dashboard/DashboardBoot.js | 26 +++--- src/hooks/index.js | 100 +--------------------- src/hooks/query/estimates.js | 2 +- src/hooks/query/invoices.js | 2 +- src/hooks/query/receipts.js | 2 +- src/hooks/utils/index.js | 9 ++ src/hooks/{async.js => utils/useAsync.js} | 27 +++--- src/hooks/utils/useLocalStorage.js | 34 ++++++++ src/hooks/utils/usePrevious.js | 15 ++++ src/hooks/utils/useRequestPdf.js | 38 ++++++++ src/hooks/utils/useUpdateEffect.js | 19 ++++ src/hooks/utils/useWatch.js | 21 +++++ src/hooks/utils/useWhen.js | 13 +++ 14 files changed, 192 insertions(+), 136 deletions(-) create mode 100644 src/hooks/utils/index.js rename src/hooks/{async.js => utils/useAsync.js} (58%) create mode 100644 src/hooks/utils/useLocalStorage.js create mode 100644 src/hooks/utils/usePrevious.js create mode 100644 src/hooks/utils/useRequestPdf.js create mode 100644 src/hooks/utils/useUpdateEffect.js create mode 100644 src/hooks/utils/useWatch.js create mode 100644 src/hooks/utils/useWhen.js diff --git a/src/components/AppIntlLoader.js b/src/components/AppIntlLoader.js index 801e3cd45..583cc9215 100644 --- a/src/components/AppIntlLoader.js +++ b/src/components/AppIntlLoader.js @@ -9,7 +9,7 @@ import * as R from 'ramda'; import { AppIntlProvider } from './AppIntlProvider'; import { useSplashLoading } from '../hooks/state'; -import { useWatch } from '../hooks'; +import { useWatchImmediate } from '../hooks'; import withDashboardActions from '../containers/Dashboard/withDashboardActions'; const SUPPORTED_LOCALES = [ @@ -90,10 +90,10 @@ function useAppLoadLocales(currentLocale) { }, [currentLocale, stopLoading]); // Watches the value to start/stop splash screen. - useWatch(isLoading, (value) => (value ? startLoading() : stopLoading()), { - immediate: true, - }); - + useWatchImmediate( + (value) => (value ? startLoading() : stopLoading()), + isLoading, + ); return { isLoading }; } @@ -116,10 +116,10 @@ function useAppYupLoadLocales(currentLocale) { }, [currentLocale, stopLoading]); // Watches the valiue to start/stop splash screen. - useWatch(isLoading, (value) => (value ? startLoading() : stopLoading()), { - immediate: true, - }); - + useWatchImmediate( + (value) => (value ? startLoading() : stopLoading()), + isLoading, + ); return { isLoading }; } @@ -144,7 +144,7 @@ function AppIntlLoader({ children }) { const { isLoading: isAppLocalesLoading } = useAppLoadLocales(currentLocale); // Detarmines whether the app locales loading. - const isLoading = isAppYupLocalesLoading && isAppLocalesLoading; + const isLoading = isAppYupLocalesLoading || isAppLocalesLoading; return ( diff --git a/src/components/Dashboard/DashboardBoot.js b/src/components/Dashboard/DashboardBoot.js index 877f67c8d..81eadb658 100644 --- a/src/components/Dashboard/DashboardBoot.js +++ b/src/components/Dashboard/DashboardBoot.js @@ -3,7 +3,7 @@ import * as R from 'ramda'; import { useUser, useCurrentOrganization } from '../../hooks/query'; import { useSplashLoading } from '../../hooks/state'; -import { useWatch, useWhen } from '../../hooks'; +import { useWatch, useWatchImmediate, useWhen } from '../../hooks'; import withAuthentication from '../../containers/Authentication/withAuthentication'; @@ -21,10 +21,8 @@ function DashboardBootJSX({ authenticatedUserId }) { } = useCurrentOrganization(); // Authenticated user. - const { - isSuccess: isAuthUserSuccess, - isLoading: isAuthUserLoading, - } = useUser(authenticatedUserId); + const { isSuccess: isAuthUserSuccess, isLoading: isAuthUserLoading } = + useUser(authenticatedUserId); // Initial locale cookie value. const localeCookie = getCookie('locale'); @@ -59,25 +57,25 @@ function DashboardBootJSX({ authenticatedUserId }) { // Splash loading when organization request loading and // applicaiton still not booted. - useWatch(isOrgLoading, (value) => { + useWatchImmediate((value) => { value && !isBooted.current && startLoading(); - }); + }, isOrgLoading); - // Splash loading when request authenticated user loading and + // Splash loading when request authenticated user loading and // application still not booted yet. - useWatch(isAuthUserLoading, (value) => { + useWatchImmediate((value) => { value && !isBooted.current && startLoading(); - }); + }, isAuthUserLoading); // Stop splash loading once organization request success. - useWatch(isCurrentOrganizationSuccess, (value) => { + useWatch((value) => { value && stopLoading(); - }); + }, isCurrentOrganizationSuccess); // Stop splash loading once authenticated user request success. - useWatch(isAuthUserSuccess, (value) => { + useWatch((value) => { value && stopLoading(); - }); + }, isAuthUserSuccess); // Once the all requests complete change the app loading state. useWhen( diff --git a/src/hooks/index.js b/src/hooks/index.js index dbb2944e0..03418357d 100644 --- a/src/hooks/index.js +++ b/src/hooks/index.js @@ -1,27 +1,8 @@ -import { isEqual } from 'lodash'; -import React, { useRef, useEffect, useMemo } from 'react'; -import useAsync from './async'; +import { useRef, useEffect, useMemo } from 'react'; import useAutofocus from './useAutofocus'; +import { useLocalStorage } from './utils/useLocalStorage'; -// import use from 'async'; - -/** - * A custom useEffect hook that only triggers on updates, not on initial mount - * Idea stolen from: https://stackoverflow.com/a/55075818/1526448 - * @param {Function} effect - * @param {Array} dependencies - */ -export function useUpdateEffect(effect, dependencies = []) { - const isInitialMount = useRef(true); - - useEffect(() => { - if (isInitialMount.current) { - isInitialMount.current = false; - } else { - effect(); - } - }, dependencies); -} +export * from './utils'; export function useIsValuePassed(value, compatatorValue) { const cache = useRef([value]); @@ -62,41 +43,7 @@ export function useCellAutoFocus(ref, autoFocus, columnId, rowIndex) { return ref; } -export * from './useRequestPdf'; -export { useAsync, useAutofocus }; - -// Hook -export function useLocalStorage(key, initialValue) { - // State to store our value - // Pass initial state function to useState so logic is only executed once - const [storedValue, setStoredValue] = React.useState(() => { - try { - // Get from local storage by key - const item = window.localStorage.getItem(key); - // Parse stored json or if none return initialValue - return item ? JSON.parse(item) : initialValue; - } catch (error) { - return initialValue; - } - }); - // Return a wrapped version of useState's setter function that ... - // ... persists the new value to localStorage. - const setValue = (value) => { - try { - // Allow value to be a function so we have same API as useState - const valueToStore = - value instanceof Function ? value(storedValue) : value; - // Save state - setStoredValue(valueToStore); - // Save to local storage - window.localStorage.setItem(key, JSON.stringify(valueToStore)); - } catch (error) { - // A more advanced implementation would handle the error case - console.log(error); - } - }; - return [storedValue, setValue]; -} +export { useAutofocus }; export function useMemorizedColumnsWidths(tableName) { const [get, save] = useLocalStorage(`${tableName}.columns_widths`, {}); @@ -106,42 +53,3 @@ export function useMemorizedColumnsWidths(tableName) { }; return [get, save, handleColumnResizing]; } - -// Hook -function usePrevious(value) { - // The ref object is a generic container whose current property is mutable ... - // ... and can hold any value, similar to an instance property on a class - const ref = useRef(); - // Store current value in ref - useEffect(() => { - ref.current = value; - }, [value]); // Only re-run if value changes - // Return previous value (happens before update in useEffect above) - return ref.current; -} - -export function useWhen(condition, callback) { - React.useEffect(() => { - if (condition) { - callback(); - } - }, [condition, callback]); -} - -export function useWhenNot(condition, callback) { - return useWhen(!condition, callback); -} - -export function useWatch(state, callback, props) { - const config = { immediate: false, ...props }; - - const previosuState = usePrevious(state); - const flag = React.useRef(false); - - React.useEffect(() => { - if (!isEqual(previosuState, state) || (config.immediate && !flag.current)) { - flag.current = true; - callback(state); - } - }, [previosuState, state, config.immediate, callback]); -} diff --git a/src/hooks/query/estimates.js b/src/hooks/query/estimates.js index 26b4ecc63..8c25c4baa 100644 --- a/src/hooks/query/estimates.js +++ b/src/hooks/query/estimates.js @@ -1,7 +1,7 @@ import { useQueryClient, useMutation } from 'react-query'; import { useRequestQuery } from '../useQueryRequest'; import useApiRequest from '../useRequest'; -import { useRequestPdf } from '../useRequestPdf'; +import { useRequestPdf } from '../utils'; import { transformPagination } from 'utils'; import t from './types'; diff --git a/src/hooks/query/invoices.js b/src/hooks/query/invoices.js index e397fd509..58580dc83 100644 --- a/src/hooks/query/invoices.js +++ b/src/hooks/query/invoices.js @@ -2,7 +2,7 @@ import { useQueryClient, useMutation } from 'react-query'; import { useRequestQuery } from '../useQueryRequest'; import { transformPagination } from 'utils'; import useApiRequest from '../useRequest'; -import { useRequestPdf } from '../useRequestPdf'; +import { useRequestPdf } from '../utils'; import t from './types'; // Common invalidate queries. diff --git a/src/hooks/query/receipts.js b/src/hooks/query/receipts.js index 66d9d5fd2..989677765 100644 --- a/src/hooks/query/receipts.js +++ b/src/hooks/query/receipts.js @@ -1,6 +1,6 @@ import { useQueryClient, useMutation } from 'react-query'; import { useRequestQuery } from '../useQueryRequest'; -import { useRequestPdf } from '../useRequestPdf'; +import { useRequestPdf } from '../utils'; import useApiRequest from '../useRequest'; import { transformPagination } from 'utils'; import t from './types'; diff --git a/src/hooks/utils/index.js b/src/hooks/utils/index.js new file mode 100644 index 000000000..dd0d41c94 --- /dev/null +++ b/src/hooks/utils/index.js @@ -0,0 +1,9 @@ + + +export * from './useLocalStorage'; +export * from './usePrevious'; +export * from './useUpdateEffect'; +export * from './useWatch'; +export * from './useWhen'; +export * from './useRequestPdf'; +export * from './useAsync'; \ No newline at end of file diff --git a/src/hooks/async.js b/src/hooks/utils/useAsync.js similarity index 58% rename from src/hooks/async.js rename to src/hooks/utils/useAsync.js index 2e2c88dc3..7a3219a27 100644 --- a/src/hooks/async.js +++ b/src/hooks/utils/useAsync.js @@ -1,6 +1,6 @@ -import {useState, useCallback, useEffect} from 'react'; +import { useState, useCallback, useEffect } from 'react'; -const useAsync = (asyncFunction, immediate = true) => { +export const useAsync = (asyncFunction, immediate = true) => { const [pending, setPending] = useState(false); const [value, setValue] = useState(null); const [error, setError] = useState(null); @@ -9,16 +9,19 @@ const useAsync = (asyncFunction, immediate = true) => { // handles setting state for pending, value, and error. // useCallback ensures the below useEffect is not called // on every render, but only if asyncFunction changes. - const execute = useCallback((...args) => { - setPending(true); - setValue(null); - setError(null); + const execute = useCallback( + (...args) => { + setPending(true); + setValue(null); + setError(null); - return asyncFunction(...args) - .then(response => setValue(response)) - .catch(error => setError(error)) - .finally(() => setPending(false)); - }, [asyncFunction]); + return asyncFunction(...args) + .then((response) => setValue(response)) + .catch((error) => setError(error)) + .finally(() => setPending(false)); + }, + [asyncFunction], + ); // Call execute if we want to fire it right away. // Otherwise execute can be called later, such as @@ -31,5 +34,3 @@ const useAsync = (asyncFunction, immediate = true) => { return { execute, pending, value, error }; }; - -export default useAsync; \ No newline at end of file diff --git a/src/hooks/utils/useLocalStorage.js b/src/hooks/utils/useLocalStorage.js new file mode 100644 index 000000000..990e90c17 --- /dev/null +++ b/src/hooks/utils/useLocalStorage.js @@ -0,0 +1,34 @@ +import React from 'react'; + +// Hook +export function useLocalStorage(key, initialValue) { + // State to store our value + // Pass initial state function to useState so logic is only executed once + const [storedValue, setStoredValue] = React.useState(() => { + try { + // Get from local storage by key + const item = window.localStorage.getItem(key); + // Parse stored json or if none return initialValue + return item ? JSON.parse(item) : initialValue; + } catch (error) { + return initialValue; + } + }); + // Return a wrapped version of useState's setter function that ... + // ... persists the new value to localStorage. + const setValue = (value) => { + try { + // Allow value to be a function so we have same API as useState + const valueToStore = + value instanceof Function ? value(storedValue) : value; + // Save state + setStoredValue(valueToStore); + // Save to local storage + window.localStorage.setItem(key, JSON.stringify(valueToStore)); + } catch (error) { + // A more advanced implementation would handle the error case + console.log(error); + } + }; + return [storedValue, setValue]; +} diff --git a/src/hooks/utils/usePrevious.js b/src/hooks/utils/usePrevious.js new file mode 100644 index 000000000..f078f0509 --- /dev/null +++ b/src/hooks/utils/usePrevious.js @@ -0,0 +1,15 @@ +import { useRef, useEffect } from 'react'; + + +// Hook +export function usePrevious(value) { + // The ref object is a generic container whose current property is mutable ... + // ... and can hold any value, similar to an instance property on a class + const ref = useRef(); + // Store current value in ref + useEffect(() => { + ref.current = value; + }, [value]); // Only re-run if value changes + // Return previous value (happens before update in useEffect above) + return ref.current; +} \ No newline at end of file diff --git a/src/hooks/utils/useRequestPdf.js b/src/hooks/utils/useRequestPdf.js new file mode 100644 index 000000000..abe49acc9 --- /dev/null +++ b/src/hooks/utils/useRequestPdf.js @@ -0,0 +1,38 @@ +import React from 'react'; +import useApiRequest from '../useRequest'; + +export const useRequestPdf = (url) => { + const apiRequest = useApiRequest(); + const [isLoading, setIsLoading] = React.useState(false); + const [isLoaded, setIsLoaded] = React.useState(false); + const [pdfUrl, setPdfUrl] = React.useState(''); + const [response, setResponse] = React.useState(null); + + React.useEffect(() => { + setIsLoading(true); + apiRequest + .get(url, { + headers: { accept: 'application/pdf' }, + responseType: 'blob', + }) + .then((response) => { + // Create a Blob from the PDF Stream. + const file = new Blob([response.data], { type: 'application/pdf' }); + + // Build a URL from the file + const fileURL = URL.createObjectURL(file); + + setPdfUrl(fileURL); + setIsLoading(false); + setIsLoaded(true); + setResponse(response); + }); + }, []); + + return { + isLoading, + isLoaded, + pdfUrl, + response, + }; +}; diff --git a/src/hooks/utils/useUpdateEffect.js b/src/hooks/utils/useUpdateEffect.js new file mode 100644 index 000000000..b7b9d4bfc --- /dev/null +++ b/src/hooks/utils/useUpdateEffect.js @@ -0,0 +1,19 @@ +import { useRef, useEffect } from 'react'; + +/** + * A custom useEffect hook that only triggers on updates, not on initial mount + * Idea stolen from: https://stackoverflow.com/a/55075818/1526448 + * @param {Function} effect + * @param {Array} dependencies + */ +export function useUpdateEffect(effect, dependencies = []) { + const isInitialMount = useRef(true); + + useEffect(() => { + if (isInitialMount.current) { + isInitialMount.current = false; + } else { + effect(); + } + }, dependencies); +} diff --git a/src/hooks/utils/useWatch.js b/src/hooks/utils/useWatch.js new file mode 100644 index 000000000..519b28ed5 --- /dev/null +++ b/src/hooks/utils/useWatch.js @@ -0,0 +1,21 @@ +import { useEffect, useRef } from 'react'; + +export function useWatch(callback, argument) { + const flag = useRef(false); + + useEffect(() => { + if (!flag.current) { + flag.current = true; + return; + } + callback(argument); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [argument]); +} + +export function useWatchImmediate(callback, argument) { + useEffect(() => { + callback(argument); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [argument]); +} diff --git a/src/hooks/utils/useWhen.js b/src/hooks/utils/useWhen.js new file mode 100644 index 000000000..b72c23fe3 --- /dev/null +++ b/src/hooks/utils/useWhen.js @@ -0,0 +1,13 @@ +import React from 'react'; + +export function useWhen(condition, callback) { + React.useEffect(() => { + if (condition) { + callback(); + } + }, [condition, callback]); +} + +export function useWhenNot(condition, callback) { + return useWhen(!condition, callback); +}