diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx b/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx index d23c3099208eb..5bc16b86b35cf 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx +++ b/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx @@ -25,6 +25,7 @@ import React, { useRef, useCallback, } from 'react'; +import useEffectEvent from 'src/hooks/useEffectEvent'; import { CSSTransition } from 'react-transition-group'; import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import PropTypes from 'prop-types'; @@ -354,39 +355,28 @@ const SqlEditor = ({ return base; }, [dispatch, queryEditor.sql, startQuery, stopQuery]); - const handleWindowResize = useCallback(() => { + const handleWindowResize = useEffectEvent(() => { setHeight(getSqlEditorHeight()); - }, []); - - const handleWindowResizeWithThrottle = useMemo( - () => throttle(handleWindowResize, WINDOW_RESIZE_THROTTLE_MS), - [handleWindowResize], - ); + }); - const onBeforeUnload = useCallback( - event => { - if ( - database?.extra_json?.cancel_query_on_windows_unload && - latestQuery?.state === 'running' - ) { - event.preventDefault(); - stopQuery(); - } - }, - [ - database?.extra_json?.cancel_query_on_windows_unload, - latestQuery?.state, - stopQuery, - ], - ); + const onBeforeUnload = useEffectEvent(event => { + if ( + database?.extra_json?.cancel_query_on_windows_unload && + latestQuery?.state === 'running' + ) { + event.preventDefault(); + stopQuery(); + } + }); useEffect(() => { // We need to measure the height of the sql editor post render to figure the height of // the south pane so it gets rendered properly setHeight(getSqlEditorHeight()); - if (!database || isEmpty(database)) { - setShowEmptyState(true); - } + const handleWindowResizeWithThrottle = throttle( + handleWindowResize, + WINDOW_RESIZE_THROTTLE_MS, + ); window.addEventListener('resize', handleWindowResizeWithThrottle); window.addEventListener('beforeunload', onBeforeUnload); @@ -395,7 +385,14 @@ const SqlEditor = ({ window.removeEventListener('resize', handleWindowResizeWithThrottle); window.removeEventListener('beforeunload', onBeforeUnload); }; - }, [database, handleWindowResizeWithThrottle, onBeforeUnload]); + // TODO: Remove useEffectEvent deps once https://github.com/facebook/react/pull/25881 is released + }, [handleWindowResize, onBeforeUnload]); + + useEffect(() => { + if (!database || isEmpty(database)) { + setShowEmptyState(true); + } + }, [database]); useEffect(() => { // setup hotkeys diff --git a/superset-frontend/src/hooks/useEffectEvent.ts b/superset-frontend/src/hooks/useEffectEvent.ts new file mode 100644 index 0000000000000..1b0dfae3de231 --- /dev/null +++ b/superset-frontend/src/hooks/useEffectEvent.ts @@ -0,0 +1,60 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +// TODO: Replace to react-use-event-hook once https://github.com/facebook/react/pull/25881 is released +import { useCallback, useRef, useEffect, useLayoutEffect } from 'react'; + +/** + * Suppress the warning when using useLayoutEffect with SSR. (https://reactjs.org/link/uselayouteffect-ssr) + */ +const useLayoutEffectWithoutDevWarnings = + typeof window === 'undefined' ? useEffect : useLayoutEffect; + +/** + * Similar to useCallback, with a few subtle differences: + * @external + * https://github.com/reactjs/rfcs/blob/useevent/text/0000-useevent.md#internal-implementation + * @example + * const onStateChanged = useEffectEvent((state: T) => log(['clicked', state])); + * + * useEffect(() => { + * onStateChanged(state); + * }, [onStateChanged, state]); + * // ^ onStateChanged is guaranteed to never change and always be up to date! + */ +export function useEffectEvent any>(handler: T) { + // Keep track of the latest callback: + const handlerRef = useRef(null); + useLayoutEffectWithoutDevWarnings(() => { + handlerRef.current = handler; + }, [handler]); + + return useCallback((...args: any[]) => { + const fn = handlerRef.current; + if (!fn) { + // Never call useEvent during the first render, ie: using it as a ref handler. + throw new Error( + 'useEvent can not be called before the first render completes. Use useCallback instead if required on the initial render.', + ); + } + + return fn(...args); + }, []) as T; +} + +export default useEffectEvent;