Как получить текущее состояние внутри useCallback при использовании useReducer? - PullRequest
2 голосов
/ 01 мая 2020

Использование перехватчиков реагирования с TypeScript, и вот минимальное представление о том, что я пытаюсь сделать: иметь список кнопок на экране, и когда пользователь нажимает на кнопку, я хочу изменить текст кнопки на «Кнопка» clicked ", а затем только повторно отображает кнопку, на которую нажали .

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

Этот код работает так, как я хочу: Если я использую useState и поддерживаю свое состояние в массиве, тогда я могу использовать Функциональное обновление в useState и получить точное поведение, которое я хочу:

import * as React from 'react';
import { IHelloWorldProps } from './IHelloWorldProps';
import { useEffect, useCallback, useState } from 'react';
import { PrimaryButton } from 'office-ui-fabric-react';

interface IMyButtonProps {
  title: string;
  id: string;
  onClick: (clickedDeviceId: string) => (event: any) => void;
}

const MyButton: React.FunctionComponent<IMyButtonProps> = React.memo((props: IMyButtonProps) => {
  console.log(`Button rendered for ${props.title}`);
  return <PrimaryButton text={props.title} onClick={props.onClick(props.id)} />;
});

interface IDevice {
  Name: string;
  Id: string;
}

const HelloWorld: React.FunctionComponent<IHelloWorldProps> = (props: IHelloWorldProps) => {

  //If I use an array for state instead of object and then use useState with Functional update, I get the result I want. 
  const initialState: IDevice[] = [];
  const [deviceState, setDeviceState] = useState<IDevice[]>(initialState);

  useEffect(() => {

    //Simulate network call to load data.
    setTimeout(() => {
      setDeviceState([{ Name: "Apple", Id: "appl01" }, { Name: "Android", Id: "andr02" }, { Name: "Windows Phone", Id: "wp03" }]);
    }, 500);

  }, []);

  const _deviceClicked = useCallback((clickedDeviceId: string) => ((event: any): void => {

    setDeviceState(prevState => prevState.map((device: IDevice) => {
      if (device.Id === clickedDeviceId) {
        device.Name = `${device.Name} clicked`;
      }

      return device;
    }));

  }), []);

  return (
    <React.Fragment>
      {deviceState.map((device: IDevice) => {
        return <MyButton key={device.Id} title={device.Name} onClick={_deviceClicked} id={device.Id} />;
      })}
    </React.Fragment>
  );
};

export default HelloWorld;

Вот желаемый результат: enter image description here

Но вот моя проблема: По моему В производственном приложении состояние поддерживается в объекте, и мы используем хук useReducer для имитации стиля компонента класса setState, где нам нужно только передать измененные свойства. Поэтому нам не нужно постоянно заменять все состояние для каждого действия.

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

import * as React from 'react';
import { IHelloWorldProps } from './IHelloWorldProps';
import { useEffect, useCallback, useReducer, useState } from 'react';
import { PrimaryButton } from 'office-ui-fabric-react';

interface IMyButtonProps {
  title: string;
  id: string;
  onClick: (clickedDeviceId: string) => (event: any) => void;
}

const MyButton: React.FunctionComponent<IMyButtonProps> = React.memo((props: IMyButtonProps) => {
  console.log(`Button rendered for ${props.title}`);
  return <PrimaryButton text={props.title} onClick={props.onClick(props.id)} />;
});

interface IDevice {
  Name: string;
  Id: string;
}

interface IDeviceState {
  devices: IDevice[];
}

const HelloWorld: React.FunctionComponent<IHelloWorldProps> = (props: IHelloWorldProps) => {

  const initialState: IDeviceState = { devices: [] };
  
  //Using useReducer to mimic class component's this.setState functionality where only the updated state needs to be sent to the reducer instead of the entire state.
  const [deviceState, setDeviceState] = useReducer((previousState: IDeviceState, updatedProperties: Partial<IDeviceState>) => ({ ...previousState, ...updatedProperties }), initialState);

  useEffect(() => {
  
    //Simulate network call to load data.
    setTimeout(() => {
      setDeviceState({ devices: [{ Name: "Apple", Id: "appl01" }, { Name: "Android", Id: "andr02" }, { Name: "Windows Phone", Id: "wp03" }] });
    }, 500);
  
  }, []);

  //Have to wrap in useCallback otherwise the "MyButton" component will get a new version of _deviceClicked for each time.
  //If the useCallback wrapper is removed from here, I see the behavior I want but then the entire device list is re-rendered everytime I click on a device.
  const _deviceClicked = useCallback((clickedDeviceId: string) => ((event: any): void => {

    //Since useCallback contains the cached version of the function before the useEffect runs, deviceState.devices is always an empty array [] here. 
    const updatedDeviceList = deviceState.devices.map((device: IDevice) => {
      if (device.Id === clickedDeviceId) {
        device.Name = `${device.Name} clicked`;
      }

      return device;
    });
    setDeviceState({ devices: updatedDeviceList });

  //Cannot add the deviceState.devices dependency here because we are updating deviceState.devices inside the function. This would mean useCallback would be useless. 
  }), []);

  return (
    <React.Fragment>
      {deviceState.devices.map((device: IDevice) => {
        return <MyButton key={device.Id} title={device.Name} onClick={_deviceClicked} id={device.Id} />;
      })}
    </React.Fragment>
  );
};

export default HelloWorld;

Вот как это выглядит: enter image description here

Поэтому мой вопрос сводится к следующему: При использовании useState внутри useCallback мы можем использовать шаблон функционального обновления и фиксировать текущее состояние (вместо того, когда был кэширован useCallback). Это возможно без указания зависимостей для useCallback.

Как мы можем сделать тоже самое при использовании useReducer? Есть ли способ получить текущее состояние внутри useCallback при использовании useReducer и без указания зависимостей для useCallback?

1 Ответ

3 голосов
/ 01 мая 2020

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

//Using useReducer to mimic class component's this.setState functionality where only the updated state needs to be sent to the reducer instead of the entire state.
const [deviceState, dispatch] = useReducer(
  (previousState, action) => action(previousState),
  initialState
);

//Have to wrap in useCallback otherwise the "MyButton" component will get a new version of _deviceClicked for each time.
//If the useCallback wrapper is removed from here, I see the behavior I want but then the entire device list is re-rendered everytime I click on a device.
const _deviceClicked = useCallback(
  (clickedDeviceId) => (event) => {
    //Since useCallback contains the cached version of the function before the useEffect runs, deviceState.devices is always an empty array [] here.
    dispatch((deviceState) => ({
      ...deviceState,
      devices: deviceState.devices.map((device) => {
        if (device.Id === clickedDeviceId) {
          device.Name = `${device.Name} clicked`;
        }

        return device;
      }),
    }));
    //no dependencies here
  },
  []
);

Ниже приведен рабочий пример:

const { useCallback, useReducer } = React;
const App = () => {
  const [deviceState, dispatch] = useReducer(
    (previousState, action) => action(previousState),
    { count: 0, other: 88 }
  );
  const click = useCallback(
    (increase) => () => {
      //Since useCallback contains the cached version of the function before the useEffect runs, deviceState.devices is always an empty array [] here.
      dispatch((deviceState) => ({
        ...deviceState,
        count: deviceState.count + increase,
      }));
      //no dependencies here
    },
    []
  );
  return (
    
      
      
      
      {JSON.stringify(deviceState)}
); }; ReactDOM.render ( , document.getElementById ('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>


<div id="root"></div>

Это не то, как вы обычно используете useReducer и не причина, по которой вы не просто использовали бы вместо этого useState в этом случае.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...