понедельник, 19 декабря 2022 г.

React Hooks

Как работают функциональные компоненты с хуками.


https://reactjs.org/docs/hooks-reference.html


---------------------------------------------------------------------

1) render

---------------------------------------------------------------------


Код тела функции компонента выполняется всегда полностью, включая объявление переменных и функций при:

- монтировании компонента,

- изменении на другие значения внешних props переданных в компонент, если значение не изменилось, то тело функции компонента не выполнится.

- при изменении значений внутреннего state компонента, но только если хотя бы одно из них изменилось на другое значение, если значение не изменилось,

   то тело функции компонента не выполнится, ререндера не произойдет. Чтобы произошло выполнение тела функции при изменении на то же самое значение

   внутреннего state нужно выполнить такой хук:


function useForceUpdate () {

    const [value, setValue] = useState(0);

    return function () {

        setValue(function (previousValue) {

           return previousValue + 1;

        });

    };

}


const forceUpdate = useForceUpdate();


handleClick () {

    setStateValue(0);

    forceUpdate();

}


---------------------------------------------------------------------

2) hooks

---------------------------------------------------------------------


Хуки могут выноситься в отдельный функции, в другие файлы, но должны всегда вызываться тольк внутри функции компонента,

поскольку React нужно, чтобы они всегда вызывались и удалялись при размонтировании в определенном порядке.



---------------------------------------------------------------------

3) useContext

---------------------------------------------------------------------


Позволяет получить значение context всем компонентам потомкам сразу без необходимости передачи значения по цепочке потомков в каждый props.


Компонент, вызывающий useContext, всегда будет перерендериваться при изменении значения контекста.


Когда ближайший <MyContext.Provider> над компонентом обновляется, этот хук вызовет повторный рендер с последним значением контекста,

переданным этому провайдеру MyContext.


Контекст использует сравнение по ссылкам, чтобы определить, когда запускать последующий рендер. Поэтому повторный рендер дочернего компонента будет производиться

при каждом повторном рендере родительского Provider-компонента, если передаваемый им объект будет создаваться каждый раз. Один из вариантов решения этой проблемы - это передача вариантов

контекст только данных примитивов из родительского компонента.


useContext(MyContext) позволяет только читать контекст и подписываться на его изменения.

Обязательно необходимо прописывать<MyContext.Provider> выше в дереве компонентов, чтобы предоставить значение для этого контекста.


import React, {useContext} from 'react';


import ReactDOM from 'react-dom';


const themes = {

    light: {

        background: "#ffffff"

    },

    dark: {

        background: "#000000"

    }

};


const ThemeContext1 = React.createContext(themes.light); // значение по умолчанию

const ThemeContext2 = React.createContext(themes.dark); // значение по умолчанию


function Child (): JSX.Element {


    const theme1 = useContext(ThemeContext1);

    const theme2 = useContext(ThemeContext2);


    return (

        <>

            <div>{theme1.background}</div>

            <br />

            <div>{theme2.background}</div>

        </>

    );


}


function Parent (): JSX.Element {

    return (

        <ThemeContext1.Provider value={themes.light}>

            <ThemeContext2.Provider value={themes.dark}>

                <Child />

            </ThemeContext2.Provider>

        </ThemeContext1.Provider>

    );

}


---------------------------------------------------------------------

4) useState

---------------------------------------------------------------------


Хранит в себе внутреннее состояние компонента.


Можно 1 раз задавать первоначальное значение при монтировании компонента.


Если первоначальное значение не задано, то оно устанваливается в undefined. При этом программа выполняется далее.


В качестве первоначального значения при монтировании компонента можно устанавливать значение, взятое из внешнего props.

Но в этом случае при последующем изменении внешннего значения из props внутреннее значение в state само обновляться не будет.


Для обновления внутреннего значения его нужно будет обновиться с помощью вызова функции setState внутри useEffect, который будет реагировать на изменение

значения из внешнего props.


Для ручного обновления значения state вызывается функция setState.


В setState можно передавать новое значение для state или функцию для сложного расчета обновления внутреннего значения.

Переданная функция для обновления также имеет доступ к предыдущему состоянию state обновляемого значения.


Функция setState не имеет функции callback, которая могла бы быть выполнена после обновления занчения и ререндеринга компонента.

Функией callback в таком случае является выполнение useEffect, который будет реагировать на изменения значения state.

На этой основе можно создать функцию, которая будет имитировать setState with callback.


Функция useState будет всегда будет возвращать последнее установленное значение.


const [value, setValue] = useState(0);


const [value, setValue] = useState(props.parentValue);


const [value, setValue] = useState(function () {

    return props.parentValue + 1;

});


function handleClick () {

    setValue(1);

}


function handleClick () {

    setValue(function (previousValue) {

        return previousValue + 1;

    });

}


useEffect(function () {

    setValue(props.parentValue);

}, [props.parentValue]);


function useStateWithCallback (initialState, alwayExecuteCallback) {


    const [state, setState] = useState(initialState);


    const callbackRef = useRef(undefined);


    const setStateAndCallback = useCallback(function setStateAndCallback (newState, callback) {

        callbackRef.current = callback;

        setState(newState);

    }, []);


    useEffect(function (): void {

        if (callbackRef.current) {

            callbackRef.current(state);

            callbackRef.current = undefined;

        }

    }, alwayExecuteCallback ? undefined : [state]);


    return [state, setStateAndCallback];


}


const [value, setValue] = useStateWithCallback(0, false);


function handleClick () {

    setValue(1, function (newState) {

        console.log(newState); // 1

    });

}


---------------------------------------------------------------------

5) useReducer

---------------------------------------------------------------------


Альтернатива для useState.

Принимает функцию редюсер типа (state, action) => newState и возвращает текущее состояние в паре с методом dispatch.


Хук useReducer обычно предпочтительнее useState, когда у вас сложная логика состояния, которая включает в себя несколько значений,

или когда следующее состояние зависит от предыдущего.


import React, {useReducer} from 'react';


const initialState = {count: 0};


function reducer (state, action) {

    switch (action.type) {

        case 'increment': return {count: state.count + 1};

        case 'decrement': return {count: state.count - 1};

        default: throw new Error();

    }

}


function Child (): JSX.Element {


    const [state, dispatch] = useReducer(reducer, initialState);


    function handleMinusClick () {

        dispatch({type: 'decrement'});

    }


    function handlePlusClick () {

        dispatch({type: 'increment'});

    }


    return (

        <>

            <div>Count: {state.count}</div>

            <button onClick={handleMinusClick}>-</button>

            <button onClick={handlePlusClick}>+</button>

        </>

    );


}


---------------------------------------------------------------------

6) useEffect

---------------------------------------------------------------------


Вызывается после завершения рендера.


Функция вызывает переданную в неё функцию каждый раз при монтировании компонента.


Затем, если в неё передан массив с зависимыми значениями, то функция вызывается каждый раз при последующих рендерингах при передаче props или изменении

внутреннего state.


Если передан массив с зависимыми значениями, то функция вызывается каждый раз при последующих рендерингах и изменении любого значения из массива.


Значения в массиве сравниваются поверхностно через строго равенство ===. Поэтому переданные в массив объекты всегда будут не равны своим предыдущим версиям.

В этом случае переданная в useEffect функция будет вызываться всегда.


Если функция useEffect возращает через return какую-то функцию, то она будет вызываться всегда при размонтировании компонента и какждый раз перед выполнением

основной переданной функции, когда прийдет момент вызова той.


В React 18 useEffect будет вызываться 2 раза, имитируя монтирование, размонтирование и повторное монтирование компонента.


Поэтому useEffect не рекомендуется использовать во всех возможных ситуациях.


// componentDidMount 1 раз

useEffect(function () {

    function log () {console.log(1);}

    log();

}, []);


// componentDidMount 1 раз и всегда componentDidUpdate

useEffect(function () {

    function log () {console.log(1);}

    log();

});


// componentDidMount 1 раз и componentDidUpdate, когда изменяется любое значение в массиве зависимостей

useEffect(function () {

    function log () {console.log(props.one + props.two);}

    log();

}, [props.one, props.two]);


// componentDidMount 1 раз и componentDidUpdate, когда изменяется любое значение в массиве зависимостей, или componentWillUnmount

useEffect(function () {


    function log () {console.log(props.one + props.two);}

    const intervalId = setInterval(function () {

        log();

    }, 1000);


    return function () { // componentWillUnmount

        clearInverval(intervalId);

    };


}, [props.one, props.two]);


---------------------------------------------------------------------

7) useLayoutEffect

---------------------------------------------------------------------


Идентичен useEffect, но в отличии от useEffect этот хук запускается синхронно после всех изменений DOM.

Используйте его для чтения макета из DOM и синхронного повторного рендеринга.

Обновления, запланированные внутри useLayoutEffect, будут полностью применены синхронно перед тем, как браузер получит шанс осуществить отрисовку.


Предпочитайте стандартный useEffect, когда это возможно, чтобы избежать блокировки визуальных обновлений.


Если вы используете серверный рендеринг, имейте в виду, что ни useLayoutEffect, ни useEffect не могут работать до загрузки JavaScript.

Вот почему React предупреждает, когда серверный компонент содержит useLayoutEffect.

Чтобы справиться с данной проблемой, либо переместите эту логику в useEffect (если она не нужна для первого рендера),

либо задержите отображение этого компонента до тех пор, пока не выполнится рендеринг на стороне клиента (если HTML некорректный до запуска useLayoutEffect).


// componentDidMount 1 раз

useLayoutEffect(function () {

    function log () {console.log(1);}

    log();

}, []);


// componentDidMount 1 раз и всегда componentDidUpdate

useLayoutEffect(function () {

    function log () {console.log(1);}

    log();

});


// componentDidMount 1 раз и componentDidUpdate, когда изменяется любое значение в массиве зависимостей

useLayoutEffect(function () {

    function log () {console.log(props.one + props.two);}

    log();

}, [props.one, props.two]);


// componentDidMount 1 раз и componentDidUpdate, когда изменяется любое значение в массиве зависимостей, или componentWillUnmount

useLayoutEffect(function () {


    function log () {console.log(props.one + props.two);}

    const intervalId = setInterval(function () {

        log();

    }, 1000);


    return function () { // componentWillUnmount

        clearInverval(intervalId);

    };


}, [props.one, props.two]);


---------------------------------------------------------------------

8) useInsertionEffect

---------------------------------------------------------------------


Аналогичен useEffect, но в отличии от useEffect, который запускается после изменения DOM, useInsertionEffect выполняется синхронно до изменения DOM.


Используется для вставки CSS стилей в DOM до того, как проичтается layout страницы.


useInsertionEffect не имеет доступ к ref и не может выполняться по расписанию.


useInsertionEffect должен использоваться только для создания библотек для css-in-js.


Вместо useInsertionEffect предпочтительней использовать useEffect или useLayoutEffect.


---------------------------------------------------------------------

9) useRef

---------------------------------------------------------------------


Используется дла хранения ссылки на тэг, как id и getElementById(), или для сохранения внутри значения переменной.


В случае передачи в тэг значению ref присваивается ссылка на этот тэг, прямой доступ к которой осуществляется через ref.current.

В ref тэга также можно передать функцию, которая будет устанавливать на него одновременно внутреннюю ссылку innerRef и внешнюю ссылку outerRef,

переданную из родительского компонента с помощью функции forwardRef.


Функция установки ref в тэг будет запускаться каждый раз после завершения рендеренга. Поэтому в ней можно прописать установку фокуса на какой-нибудь другой тэг

ref.current.parentElement.focus() или выполнить прокрутку scroll блока div.


В случае хранения в ref просто значения переменной прямой доступ к этому значения осуществляется через ref.current.


В useRef можно устанавливать 1 раз первоначально заданное значение. Оно само не будет обновляться при последующих рендренгах.


Для изменения значения ref нужно присвоить ей другое значение в ref.current.

Ручное изенение значения ref.current, в отличии от изменения внутреннего state или внешних props, не будет вызывать ререндеренг компонента.


Ref будет существовать тех пор, пока компонент не будет размонтирован.


// tag ref

const inputRef = useRef(null);


function handleClick () {

    inputRef.current.focus();

}


return <input type="text" ref={inputRef} />



// variable ref

const variableRef = useRef(100);


function handleClick () {

    variableRef.current = variableRef.current + 10; // 110

}


// append remove tag ref

const divRef = useRef(document.createElement('div'));


useEffect(function () {

    document.body.appendChild(divRef);


    return function () {

        document.body.removeChild(divRef);

    };

}, []);



// callback ref

function useStateWithCallback (initialState, alwayExecuteCallback) {


    const [state, setState] = useState(initialState);


    const callbackRef = useRef(undefined);


    const setStateAndCallback = useCallback(function setStateAndCallback (newState, callback) {

        callbackRef.current = callback;

        setState(newState);

    }, []);


    useEffect(function (): void {

        if (callbackRef.current) {

            callbackRef.current(state);

            callbackRef.current = undefined;

        }

    }, alwayExecuteCallback ? undefined : [state]);


    return [state, setStateAndCallback];


}


const [value, setValue] = useStateWithCallback(0, false);


function handleClick () {

    setValue(1, function (newState) {

        console.log(newState); // 1

    });

}


// inner, outer refs

const innerInputRef = useRef(null);


function setRef (element) {

    innerInputRef.current = element;

    if (outerInputRef) {

        if (typeof outerInputRef === 'function') {

            outerInputRef(element);

        } else {

            outerInputRef.current = element;

        }

    }

}


<input type="text" ref={setRef} />


---------------------------------------------------------------------

10) React.forwardRef

---------------------------------------------------------------------


Позволяет передать внешний Ref из родительского компонента в дочерний.


export const DivComponent: React.FC<Props> = forwardRef<HTMLDivElement, Props>(

    function DivComponent (props: Props, outerRef: ForwardedRef<HTMLDivElement>): JSX.Element {


        const innerRef: React.MutableRefObject<HTMLDivElement> = useRef<HTMLDivElement>(null);


        function setRef (element: HTMLDivElement): void {

            innerRef.current = element;

            if (outerRef) {

                if (typeof outerRef === 'function') {

                    outerRef(element);

                } else {

                    outerRef.current = element;

                }

            }

        }


        return (

            <div ref={setRef}>block</div>

        );


    }

);



---------------------------------------------------------------------

11) useCallback

---------------------------------------------------------------------


Во время выполнения тела функции компонента все его элементы внутри также пересоздаются.


Чтобы оптимизировать процесс пересоздания функций используется useCallback, который пересоздает тело функции только после изменения хотя бы одного из

значений в массиве зависимостей.


Функция всегда создается 1 раз при первом рендеренге компонента. И в дальнейшем пересоздается только, если изменились значения в массиве зависимостей.


const sendMessage = useCallback(function () {

    console.log(props.message + ' to ' + props.address);

}, [props.message, props.address]);


function handleClick () {

    sendMessage();

}


---------------------------------------------------------------------

12) useMemo

---------------------------------------------------------------------


Во время выполнения тела функции компонента все его элементы внутри также пересоздаются.


При этом вычисления значений переменных могут быть весьма затратны и длительны.


Чтобы оптимизировать процесс повторного вычисления значений переменных используется useMemo, которая заново вычисляет значение переменной только после изменения хотя бы одного из

значений в массиве зависимостей.


Переменная всегда создается 1 раз при первом рендеренге компонента. И в дальнейшем пересоздается только, если изменились значения в массиве зависимостей.


const bigArray = useMemo(function () {

    return new Array(1000).map(function (id) {

        return id + ') ' + props.message + ' to ' + props.address;

    });

}, [props.message, props.address]);


function handleClick () {

    console.log(bigArray);

}


---------------------------------------------------------------------

13) React.createPortal

---------------------------------------------------------------------


Помещает JSX-тэги внутрь обычного тэга.


interface Props {

    children: React.ReactNode;

}


function Portal (props: Props): JSX.Element {


    const container: React.MutableRefObject<HTMLDivElement> = useRef<HTMLDivElement>(document.createElement('div'));


    useEffect(function (): () => void {

        document.body.appendChild(container.current);

        const elementForRemoval: HTMLDivElement = container.current;


        return function (): void {

            document.body.removeChild(elementForRemoval);

        };

    }, [container]);


    return ReactDOM.createPortal(props.children, container.current);


}


---------------------------------------------------------------------

14) React.Children, React.cloneElement

---------------------------------------------------------------------


Позволяет обработать входящих props.children и изменить из props.


</div>

    {

        React.Children.map(props.children, function (child: React.ReactElement<TabProps>): React.ReactElement<TabProps> {

            return React.cloneElement(

                child,

                {

                    ...child.props,

                    selectedTabId: props.selectedTabId,

                    onChange: props.onChange

                }

            );

        })

    }

</div>


---------------------------------------------------------------------

15) useDebugValue

---------------------------------------------------------------------


Можно использовать в собственных хуках при отладке через React Dev Tools для вывода туда переданного в хук useDebugValue значения.


function useNetworkStatus () {


    const [isOnline, setIsOnline] = useState(null);


    useDebugValue(isOnline ? 'В сети' : 'Не в сети'); // В React Dev Tools выведется: NetworkStatus: "Не в сети"

    // useDebugValue(date, function (date) {return date.toDateString();}); // Можно передавать функцию для форматирования значения перед его выводом в React Dev Tools.


    return isOnline;


}


---------------------------------------------------------------------

16) useDeferredValue

---------------------------------------------------------------------


Делает отсрочку в применении обновления переданного значения например при быстром вводе символов в <input>


function Typeahead () {


    const query = useSearchQuery('');

    const deferredQuery = useDeferredValue(query);


    // useMemo позволяет обновить значение только когда изменится отложенное по времени deferredQuery, а не просто каждый query.

    const suggestions = useMemo(function () {

        <SearchSuggestions query={deferredQuery} />

    }, [deferredQuery]);


    return (

        <>

            <SearchInput query={query} />

            <Suspense fallback="Loading results...">{suggestions}</Suspense>

        </>

    );


}


---------------------------------------------------------------------

17) useTransition

---------------------------------------------------------------------


Возвращает функцию для запуска процесса перехода от одного состояния к другому и переменную, хранящую в себе статус состояния перехода.


function App () {


    const [count, setCount] = useState(0);


    const [isPending, startTransition] = useTransition();


    function handleClick () {

        startTransition(function () {

            setCount(function (previosCount) {

                return previosCount + 1;

            });

        });

    }


    return (

        <>

            <div>{isPending && <Spinner />}</div>

            <div>{count}</div>

            <button onClick={handleClick}>[+]</button>

        </>

    );

}


---------------------------------------------------------------------

18) useId

---------------------------------------------------------------------


Просто генерирует уникальные ID, которые стабильны на сервере и на клиенте для избегания несовпадения при гидрации.

useId нельзя использовать для генерирования ключей массивов.


function Fields () {

    const id = useId();

    return (

        <div>

            <label htmlFor={id + '-firstName'}>First Name</label>

            <input id={id + '-firstName'} type="text" />

            <br />

            <label htmlFor={id + '-lastName'}>Last Name</label>

            <input id={id + '-lastName'} type="text" />

        </div>

    );

}


---------------------------------------------------------------------

19) useSyncExternalStore

---------------------------------------------------------------------


useSyncExternalStore рекомендован для использования в качестве чтения и подписки на внешние источники данных.


Хук возвращает значение и принимает 3 аргумента:

- subscribe - функци для регистрации callback, который будет вызван при изменении данных в источнике данных store.

- getSnapshot - функция, которая вернет конкретное значение из источника данных store.

- getServerSnapshot - фукникция, которая вернет снимок snapshot в течении серверного рендеринга.


Когда производится серверный рендерингг, то вам надо сериализовать значение на сервере с помощью useSyncExternalStore.

React в этом случае используется snapshot в процессе hydration для предотвращения ошибок.


const selectedField = useSyncExternalStore(

    store.subscribe,

    function () {return store.getSnapshot().selectedField;},

    function () {return INITIAL_SERVER_SNAPSHOT.selectedField;}

);


---------------------------------------------------------------------

20) useOutsideClickEventHandler

---------------------------------------------------------------------


import React, {useEffect} from 'react';


export function useOutsideClickEventHandler <T extends HTMLElement = HTMLElement> (ref: React.RefObject<T>, handler: (event: Event) => void): void {

    useEffect(function (): () => void {


        function handleClickOutside (event: Event): void {

            if (ref.current && !ref.current.contains(event.target as Node)) {

                handler(event);

            }

        }


        document.addEventListener('mousedown', handleClickOutside);


        return function (): void {

            document.removeEventListener('mousedown', handleClickOutside);

        };


    }, [ref, handler]);

}


---------------------------------------------------------------------

21) useReduxState

---------------------------------------------------------------------


import {useSelector} from 'react-redux';


import get from 'lodash-es/get';


import {RootStateInterface} from './interfaces';


type RootStateKey = keyof RootStateInterface;

type StateKeyWithDots = `.${string}`;

type RootOrDotsStateKey = RootStateKey | StateKeyWithDots;


interface SelectedStateInterface extends Partial<RootStateInterface> {

    [key: StateKeyWithDots]: any;

}


export function useReduxState (firstStateKey: RootOrDotsStateKey, ...stateKeys: RootOrDotsStateKey[]): SelectedStateInterface {

    stateKeys.push(firstStateKey);

    const selectedState: SelectedStateInterface = {};

    stateKeys.forEach(function (stateKey: RootOrDotsStateKey): void {

        selectedState[stateKey] = useSelector(function (state: RootStateInterface): any {

            if (stateKey.indexOf('.') === 0) {

                return get(state, stateKey.slice(1));

            }

            return state[stateKey];

        });

    });

    return selectedState;

}


const {

    input: {

        value: inputValue,

        date: calendarDate

    }

} = useReduxState('calendar');

Комментариев нет:

Отправить комментарий