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

У меня есть простой компонент на основе классов, который я пытаюсь преобразовать в компонент на основе функций, но я захожу в тупики.

Мой компонент представляет собой прямую адаптацию стандартного пакета gifted-chat и использует Watson Assistant в качестве бэкэнда для предоставления ответов. В бэкэнд-части нет ничего сложного, это всего лишь тонкие обертки API Watson Assistants:

getSessionID = async (): Promise<string>

получает идентификатор сеанса для использования при взаимодействии с серверной частью, и

sendReply = async (reply: string, sessionID: string): Promise<string>

возвращает ответ Ассистента на строку, представленную как reply. Это не источник моей проблемы (тела обоих можно заменить на return await "some string", и у меня возникнут те же проблемы): версия на основе классов (ниже) работает отлично.

Но я не понимаю, как преобразовать это в функциональную форму, в частности:

  1. Я изо всех сил пытаюсь найти подходящую замену для componentWillMount. Использование useEffect с sessionID в качестве состояния приводит к ошибкам: вызывается getMessage (даже если я await) до того, как будет установлен требуемый идентификатор сеанса.

Я могу избежать этого, не создавая состояние sessionID (которого, возможно, не должно быть), а просто сделав его глобальным (как в функциональной попытке ниже). Но даже если я сделаю это:

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

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


Chatter.tsx (версия для рабочего класса)

import React from 'react'
import { GiftedChat } from 'react-native-gifted-chat'
import WatsonAssistant from "../services/WatsonAssistant"



class Chatter extends React.Component {
    state = {
        messages: [],
        sessionID: null,
    }

    componentWillMount() {
        WatsonAssistant.getSessionID()
            .then((sID) => {    
                this.setState( {
                    sessionID: sID,
                } )    
            } )
            .then(() => this.getMessage(''))
            .catch((error) => {
                console.error(error)
            } )
    }

    onSend = (message = []): void => {
        this.setState((previousState) => ( {
            messages: GiftedChat.append(previousState.messages, message),
        } ), () => {
            this.getMessage(message[0].text.replace(/[\n\r]+/g, ' '))
        } )
    }

    getMessage = async (text: string): Promise<void> => {
        let response = await WatsonAssistant.sendReply(text, this.state.sessionID)
        let message = {
            _id: Math.round(Math.random() * 1000000).toString(),
            text: response,
            createdAt: new Date(),
            user: {
                _id: '2',
                name: 'Watson Assistant',
            },
        }
        this.setState((previousState) => ( {
            messages: GiftedChat.append(previousState.messages, message),
        } ))
    }

    render() {
        return (
            <GiftedChat
                messages={ this.state.messages }
                onSend={ messages => this.onSend(messages) }
                user={ {
                    _id: 1,
                } }
            />
        )
    }
}

export default Chatter

Chatter.tsx (неудачная попытка на основе функции)

import React, {FC, ReactElement, useEffect, useState } from 'react'
import { GiftedChat } from 'react-native-gifted-chat'
import WatsonAssistant from "../services/WatsonAssistant"

let sessionID: string

const Chatter: FC = (): ReactElement => {

    const [ messages, setMessages ] = useState([])

    useEffect(() => {
        const fetchData = async () => {
            WatsonAssistant.getSessionID()
                .then(sID => sessionID = sID )
                .then(() => getMessage(''))
                .catch((error) => {
                    console.error(error)
                } )
        }
        fetchData()
    }, [ ])

    const onSend = async (message = []) => {
        const newMessages = await GiftedChat.append(messages, message)
        await setMessages(newMessages)
        await getMessage(message[0].text.replace(/[\n\r]+/g, ' '))
    }

    const getMessage = async (text: string): Promise<void> => {
        let response = await WatsonAssistant.sendReply(text, sessionID)
        let message = {
            _id: Math.round(Math.random() * 1000000).toString(),
            text: response,
            createdAt: new Date(),
            user: {
                _id: '2',
                name: 'Watson Assistant',
            },
        }
        await setMessages(await GiftedChat.append(messages, message))
    }


    return (
        <GiftedChat
            messages={ messages }
            onSend={ messages => onSend(messages) }
            user={ {
                _id: 1,
            } }
        />
    )

}

export default Chatter

Chatter.tsx (версия на основе рабочих функций)

import React, {FC, ReactElement, useEffect, useState } from 'react'
import { GiftedChat } from 'react-native-gifted-chat'
import WatsonAssistant from "../services/WatsonAssistant"

let sessionID: string

const Chatter: FC = (): ReactElement => {

    const [ messages, setMessages ] = useState([])

    useEffect(() => {
        const fetchData = async () => {
            WatsonAssistant.getSessionID()
                .then(sID => sessionID = sID )
                .then(() => getMessage('', []))
                .catch((error) => {
                    console.error(error)
                } )
        }
        fetchData()
    }, [ ])

    const onSend = async (message = []) => {
        const newMessages = await GiftedChat.append(messages, message)
        await setMessages(newMessages)   // Apparently, no waiting goes on here
        await getMessage(message[0].text.replace(/[\n\r]+/g, ' '), newMessages)
    }

    const getMessage = async (text: string, currentMessages): Promise<void> => {
        let response = await WatsonAssistant.sendReply(text, sessionID)
        let message = {
            _id: Math.round(Math.random() * 1000000).toString(),
            text: response,
            createdAt: new Date(),
            user: {
                _id: '2',
                name: 'Watson Assistant',
            },
        }
        await setMessages(await GiftedChat.append(currentMessages, message))
    }


    return (
        <GiftedChat
            messages={ messages }
            onSend={ messages => onSend(messages) }
            user={ {
                _id: 1,
            } }
        />
    )

}

export default Chatter

person orome    schedule 25.08.2019    source источник
comment
(Примечание: я новичок во всем этом, поэтому, возможно, не хватает некоторых основ.)   -  person orome    schedule 25.08.2019
comment
Не ответ, но попробуйте переместить все через useEffect. Также попробуйте установить sessionID в состояние (как в классе на основе), а затем прослушайте это изменение с помощью другого useEffect, чтобы затем вызвать getMessage.   -  person Alvaro    schedule 25.08.2019
comment
@Alvaro Я также просматриваю react-usestate-callback. Есть ли причина избегать этого?   -  person orome    schedule 25.08.2019
comment
@orome, потому что useStateFromCallback - это просто эквивалент useState и useEffect(foobar, [myStateValue]), как вы можете видеть в исходном коде библиотеки: github.com/the-road-to-learn-react/use-state-with-callback/blob/   -  person Victor Levasseur    schedule 25.08.2019
comment
@VictorLevasseur А, конечно, понятно. Думаю, это мне не поможет.   -  person orome    schedule 25.08.2019


Ответы (1)


Хорошо, поскольку у меня нет вашего полного кода, я не уверен, что он будет работать как есть (в частности, без типов из ваших зависимостей, я не уверен, будет ли / сколько компилятор будет жаловаться), но должен дать вы что-то можете приспособить достаточно легко.

const reducer = ({ messages }, action) => {
  switch (action.type) {
    case 'add message':
      return {
        messages: GiftedChat.append(messages, action.message),
      };

    case 'add sent message':
      return {
        // Not sure if .append is variadic, may need to adapt
        messages: GiftedChat.append(messages, action.message, action.message[0].text.replace(/[\n\r]+/g, ' ')),
      }
  }
};

const Chatter = () => {
  const [sessionID, setSessionID] = useState(null);
  const [messages, dispatch] = useReducer(reducer, []);

  const getMessage = async (text: string, sessionID: number, type: string = 'add message'): Promise<void> => {
    const response = await WatsonAssistant.sendReply(text, sessionID);
    const message = {
      _id: Math.round(Math.random() * 1000000).toString(),
      text: response,
      createdAt: new Date(),
      user: {
        _id: '2',
        name: 'Watson Assistant',
      },
    };

    dispatch({
      type,
      message,
    });
  };

  useEffect(() => {
    const fetchData = async () => {
      WatsonAssistant.getSessionID()
        .then(sID => (setSessionID(sID), sID))
        .then(sID => getMessage('', sID))
        .catch((error) => {
          console.error(error)
        });
    }
    fetchData();
  }, []);

  return (
    <GiftedChat
      messages={messages}
      onSend={messages => getMessage(messages, sessionID, 'add sent message')}
      user={{
        _id: 1,
      }}
    />
  );
};

Основное отличие - useReducer. Насколько я могу судить, в исходном коде у вас было два действия: добавить это сообщение или добавить это сообщение, а затем его копию с замененным текстовым регулярным выражением. Я использовал разные отправки редуктору для обработки случаев, а не обратный вызов setState. Я изменил вашу попытку useEffect, здесь я (ab) использую оператор запятой, чтобы вернуть идентификатор, возвращенный из службы, чтобы его можно было напрямую передать в getMessage в качестве параметра, а не полагаться на состояние, которое не было пока не обновлялся.

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

person Jared Smith    schedule 25.08.2019
comment
Почему бы не назначить sessionID на ссылку и не установить его в then()? Таким образом, нам не нужно передавать его в качестве аргумента в getMessage, и, насколько я понимаю, это не повлияет ни на что другое. - person Alvaro; 25.08.2019
comment
Это интересный подход (я не могу заставить его работать, но я попробую). Но более интересная часть вашего ответа заключается в том, что я по-прежнему скептически отношусь к API хуков. В чем суть крючков и переход от к функциональным компонентам. В таком случае компонент на основе классов намного легче понять и управлять; функциональная версия просто сбивает с толку. - person orome; 25.08.2019
comment
@Alvaro, который, вероятно, сработает, поскольку вы устанавливаете его только один раз при монтировании, и он никогда не меняется. - person Jared Smith; 25.08.2019
comment
@orome Думаю, я очень скептически отношусь к useEffect. Все остальные кажутся довольно простыми, и мне особенно нравятся состояние, редуктор и контекст. Но очень, очень сложно (по крайней мере, для меня, по крайней мере, сейчас) получить правильную семантику useEffect. Он запускается, когда я этого не хочу, он не запускается, когда я этого хочу, и в половине случаев он входит в бесконечный цикл повторного рендеринга. Я подозреваю, что он слишком мощный / гибкий, и поэтому API слишком странный. - person Jared Smith; 25.08.2019
comment
Обратите внимание, что я могу заставить мою функциональную версию работать нормально, если я добавлю аргумент в getMessage для только что обновленных сообщений, передав результат GiftedChat.append(messages, message) как значение в onSend. Моя проблема, я думаю, сводится к тому, что либо мой await на setMessages не действует, либо setMessages вызывает побочный эффект, который меня сбивает. В любом случае, без изменения, последующий getMessage внутри onSend использует старое состояние, если в качестве аргумента не передаются новые сообщения (не состояние сообщений). (Ничего подобного я не понимаю.) - person orome; 25.08.2019
comment
Состояние настройки @orome (в любом случае) не возвращает обещание (и они отказались изменить его, чтобы сделать это по веским причинам, которые я не припоминаю, это ветка github, если вам интересно). Таким образом, нет возможности await изменить состояние, для чего был использован параметр обратного вызова / componentDidUpdate. Вы можете смоделировать это с помощью useEffect, но не обязательно, что вам это нужно здесь. - person Jared Smith; 25.08.2019
comment
@JaredSmith Да, я думаю, вот к чему все сводится. Я устанавливаю состояние, а затем сразу полагаюсь на него (до того, как оно действительно изменилось), поэтому, если я явно не передам значение, которое я использовал, также просто передано setMessages в качестве первого аргумента для GiftedChat.append, я (вероятно) использую состояние, которое hasn ' t был обновлен. - person orome; 25.08.2019