Реагировать на получение асинхронных данных useReducer

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

Как разместить выборку данных в операторе switch или это не так, как это нужно делать?

import React from 'react'

const ProfileContext = React.createContext()

const initialState = {
  data: false
}

let reducer = async (state, action) => {
  switch (action.type) {
    case 'unload':
      return initialState
    case 'reload':
      return { data: reloadProfile() } //how to do it???
  }
}


const reloadProfile = async () => {
  try {
    let profileData = await fetch('/profile')
    profileData = await profileData.json()

    return profileData
  } catch (error) {
    console.info(error)
  }
}

function ProfileContextProvider(props) {
  let [profile, profileR] = React.useReducer(reducer, initialState)

  return (
    <ProfileContext.Provider value = {{ profile, profileR }}>
      {props.children}
    </ProfileContext.Provider>
  )
}

export { ProfileContext, ProfileContextProvider }

Я пытался сделать это вот так, но он не работает с async; (

let reducer = async (state, action) => {
  switch (action.type) {
    case 'unload':
      return initialState
    case 'reload': {
      return await { data: 2 }
    }
  }
}

Я думаю, вы хотите, чтобы ваш редуктор был синхронным. Возможно, вы могли бы установить значение, например, loading в true в случае reload и в вашем компоненте имеют эффект, который повторно запускается при изменении loading, например useEffect(() => { if (loading) { reloadProfile().then(...) } }, [loading]);

Tholle 05.11.2018 01:29

Возможно, полезно для тех, кто сталкивается с этим вопросом: robinwieruch.de/react-hooks-fetch-data

Robin Wieruch 15.04.2019 19:07
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Навигация по приложениям React: Исчерпывающее руководство по React Router
Навигация по приложениям React: Исчерпывающее руководство по React Router
React Router стала незаменимой библиотекой для создания одностраничных приложений с навигацией в React. В этой статье блога мы подробно рассмотрим...
Массив зависимостей в React
Массив зависимостей в React
Все о массиве Dependency и его связи с useEffect.
97
2
68 243
7
Перейти к ответу Данный вопрос помечен как решенный

Ответы 7

Это интересный случай, которого не затрагивают примеры useReducer. Я не думаю, что редуктор - подходящее место для асинхронной загрузки. Исходя из мышления Redux, вы обычно загружаете данные в другое место, либо в преобразователь, наблюдаемый (например, redux-observable), либо просто в событие жизненного цикла, такое как componentDidMount. С новым useReducer мы могли использовать подход componentDidMount, используя useEffect. Ваш эффект может быть примерно таким:

function ProfileContextProvider(props) {
  let [profile, profileR] = React.useReducer(reducer, initialState);

  useEffect(() => {
    reloadProfile().then((profileData) => {
      profileR({
        type: "profileReady",
        payload: profileData
      });
    });
  }, []); // The empty array causes this effect to only run on mount

  return (
    <ProfileContext.Provider value = {{ profile, profileR }}>
      {props.children}
    </ProfileContext.Provider>
  );
}

Также рабочий пример здесь: https://codesandbox.io/s/r4ml2x864m.

Если вам нужно передать опору или состояние в функцию reloadProfile, вы можете сделать это, настроив второй аргумент для useEffect (пустой массив в примере), чтобы он запускался только при необходимости. Вам нужно будет либо проверить предыдущее значение, либо реализовать какой-то кеш, чтобы избежать ненужной выборки.

Обновление - перезагрузка из дочернего

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

function ProfileContextProvider(props) {
  let [profile, profileR] = React.useReducer(reducer, initialState);

  const onReloadNeeded = useCallback(async () => {
    const profileData = await reloadProfile();
    profileR({
      type: "profileReady",
      payload: profileData
    });
  }, []); // The empty array causes this callback to only be created once per component instance

  useEffect(() => {
    onReloadNeeded();
  }, []); // The empty array causes this effect to only run on mount

  return (
    <ProfileContext.Provider value = {{ onReloadNeeded, profile }}>
      {props.children}
    </ProfileContext.Provider>
  );
}

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

function ProfileContextProvider(props) {
  let [profile, profileR] = React.useReducer(reducer, initialState);

  const customDispatch= useCallback(async (action) => {
    switch (action.type) {
      case "reload": {
        const profileData = await reloadProfile();
        profileR({
          type: "profileReady",
          payload: profileData
        });
        break;
      }
      default:
        // Not a special case, dispatch the action
        profileR(action);
    }
  }, []); // The empty array causes this callback to only be created once per component instance

  return (
    <ProfileContext.Provider value = {{ profile, profileR: customDispatch }}>
      {props.children}
    </ProfileContext.Provider>
  );
}

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

ZiiMakc 05.11.2018 10:15

Я добавил несколько примеров предоставления дочерним компонентам метода перезагрузки данных в родительском элементе. Отвечает ли это на ваш вопрос?

Tyler 05.11.2018 12:25

да, спасибо, Работаю, когда я добавил перерыв; перезагрузить футляр!

ZiiMakc 05.11.2018 12:48

Вы хотите избежать использования useEffect(async () => {}). Первый оператор return функции в useEffect предназначен для очистки, и это всегда немедленно возвращает обещание. Это будет предупреждать (и, возможно, не работать), когда хуки активны.

Nate 10.01.2019 20:25

Хороший улов, Нейт! Я забыл про функцию очистки. Я обновил свой ответ, чтобы не возвращать Promise in useEffect.

Tyler 12.01.2019 03:07

вот очень хорошая статья, которая также применяет тот же подход medium.com/@patrick.gross1987/…

Alberto S. 30.07.2020 18:04

Не OP, но спасибо за этот ответ! Это было именно то, что я искал.

Robert Vunabandi 05.10.2020 05:38

Не могу понять, почему в случае Перезагрузить из ребенка почему бы просто не использовать useState, поскольку поставщик контекста ведет себя так же, как редуктор?

Yonatan Tuchinsky 19.11.2020 09:31

Я написал очень подробное объяснение проблемы и возможных решений. Дэн Абрамов предложил Решение 3.

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

https://gist.github.com/astoilkov/013c513e33fe95fa8846348038d8fe42

Я обернул метод отправки слоем для решения проблемы асинхронного действия.

Вот начальное состояние. Ключ loading записывает текущий статус загрузки приложения. Это удобно, когда вы хотите показать страницу загрузки, когда приложение получает данные с сервера.

{
  value: 0,
  loading: false
}

Есть четыре вида действий.

function reducer(state, action) {
  switch (action.type) {
    case "click_async":
    case "click_sync":
      return { ...state, value: action.payload };
    case "loading_start":
      return { ...state, loading: true };
    case "loading_end":
      return { ...state, loading: false };
    default:
      throw new Error();
  }
}
function isPromise(obj) {
  return (
    !!obj &&
    (typeof obj === "object" || typeof obj === "function") &&
    typeof obj.then === "function"
  );
}

function wrapperDispatch(dispatch) {
  return function(action) {
    if (isPromise(action.payload)) {
      dispatch({ type: "loading_start" });
      action.payload.then(v => {
        dispatch({ type: action.type, payload: v });
        dispatch({ type: "loading_end" });
      });
    } else {
      dispatch(action);
    }
  };
}

Допустим, есть асинхронный метод

async function asyncFetch(p) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(p);
    }, 1000);
  });
}

wrapperDispatch(dispatch)({
  type: "click_async",
  payload: asyncFetch(new Date().getTime())
});

Полный пример кода находится здесь:

https://codesandbox.io/s/13qnv8ml7q

Ответ принят как подходящий

Это хорошая практика для держать редукторы в чистоте. Это сделает useReducer более предсказуемым и упростит тестирование. В обоих последующих подходах асинхронные операции сочетаются с чистыми редукторами:

1. Получить данные до dispatch (простой)

Оберните исходный dispatch в asyncDispatch и позвольте контексту передать эту функцию:

const AppContextProvider = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, initState);
  const asyncDispatch = () => { // adjust args to your needs
    dispatch({ type: "loading" });
    fetchData().then(data => {
      dispatch({ type: "finished", payload: data });
    });
  };
  
  return (
    <AppContext.Provider value = {{ state, dispatch: asyncDispatch }}>
      {children}
    </AppContext.Provider>
  );
  // Note: memoize the context value, if Provider gets re-rendered more often
};

const reducer = (state, { type, payload }) => {
  if (type === "loading") return { status: "loading" };
  if (type === "finished") return { status: "finished", data: payload };
  return state;
};

const initState = {
  status: "idle"
};

const AppContext = React.createContext();

const AppContextProvider = ({ children }) => {
  const [state, dispatch] = React.useReducer(reducer, initState);
  const asyncDispatch = () => { // adjust args to your needs
    dispatch({ type: "loading" });
    fetchData().then(data => {
      dispatch({ type: "finished", payload: data });
    });
  };

  return (
    <AppContext.Provider value = {{ state, dispatch: asyncDispatch }}>
      {children}
    </AppContext.Provider>
  );
};

function App() {
  return (
    <AppContextProvider>
      <Child />
    </AppContextProvider>
  );
}

const Child = () => {
  const val = React.useContext(AppContext);
  const {
    state: { status, data },
    dispatch
  } = val;
  return (
    <div>
      <p>Status: {status}</p>
      <p>Data: {data || "-"}</p>
      <button onClick = {dispatch}>Fetch data</button>
    </div>
  );
};

function fetchData() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(42);
    }, 2000);
  });
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity = "sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4 = " crossorigin = "anonymous"></script>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity = "sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w = " crossorigin = "anonymous"></script>
<div id = "root"></div>

2. Используйте промежуточное ПО для dispatch (универсальное).

dispatch может быть расширен с помощью промежуточное ПО, например редукционный преобразователь, наблюдаемый при редукции, редукс-сага, для большей гибкости и возможности повторного использования. Или один напиши свой.

Скажем, мы хотим 1.) получить асинхронные данные с помощью redux-thunk 2.) сделать запись в журнал 3.) вызвать dispatch с окончательным результатом. Сначала определите промежуточное ПО:

import thunk from "redux-thunk";
const middlewares = [thunk, logger]; // logger is our own implementation

Затем напишите собственный useMiddlewareReducer Hook, который вы можете увидеть здесь как useReducer в комплекте с дополнительным промежуточным программным обеспечением, похожим на Redux applyMiddleware:

const [state, dispatch] = useMiddlewareReducer(middlewares, reducer, initState);

Промежуточное ПО передается в качестве первого аргумента, в противном случае API такой же, как useReducer. Для реализации берем applyMiddlewareисходный код и переносим в React Hooks.

const middlewares = [ReduxThunk, logger];

const reducer = (state, { type, payload }) => {
  if (type === "loading") return { ...state, status: "loading" };
  if (type === "finished") return { status: "finished", data: payload };
  return state;
};

const initState = {
  status: "idle"
};

const AppContext = React.createContext();

const AppContextProvider = ({ children }) => {
  const [state, dispatch] = useMiddlewareReducer(
    middlewares,
    reducer,
    initState
  );
  return (
    <AppContext.Provider value = {{ state, dispatch }}>
      {children}
    </AppContext.Provider>
  );
};

function App() {
  return (
    <AppContextProvider>
      <Child />
    </AppContextProvider>
  );
}

const Child = () => {
  const val = React.useContext(AppContext);
  const {
    state: { status, data },
    dispatch
  } = val;
  return (
    <div>
      <p>Status: {status}</p>
      <p>Data: {data || "-"}</p>
      <button onClick = {() => dispatch(fetchData())}>Fetch data</button>
    </div>
  );
};

function fetchData() {
  return (dispatch, getState) => {
    dispatch({ type: "loading" });
    setTimeout(() => {
      // fake async loading
      dispatch({ type: "finished", payload: (getState().data || 0) + 42 });
    }, 2000);
  };
}

function logger({ getState }) {
  return next => action => {
    console.info("state:", JSON.stringify(getState()), "action:", JSON.stringify(action));
    return next(action);
  };
}

// same API as useReducer, with middlewares as first argument
function useMiddlewareReducer(
  middlewares,
  reducer,
  initState,
  initializer = s => s
) {
  const [state, setState] = React.useState(initializer(initState));
  const stateRef = React.useRef(state); // stores most recent state
  const dispatch = React.useMemo(
    () =>
      enhanceDispatch({
        getState: () => stateRef.current, // access most recent state
        stateDispatch: action => {
          stateRef.current = reducer(stateRef.current, action); // makes getState() possible
          setState(stateRef.current); // trigger re-render
          return action;
        }
      })(...middlewares),
    [middlewares, reducer]
  );

  return [state, dispatch];
}

//                                                         |  dispatch fn  |
// A middleware has type (dispatch, getState) => nextMw => action => action
function enhanceDispatch({ getState, stateDispatch }) {
  return (...middlewares) => {
    let dispatch;
    const middlewareAPI = {
      getState,
      dispatch: action => dispatch(action)
    };
    dispatch = middlewares
      .map(m => m(middlewareAPI))
      .reduceRight((next, mw) => mw(next), stateDispatch);
    return dispatch;
  };
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity = "sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4 = " crossorigin = "anonymous"></script>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity = "sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w = " crossorigin = "anonymous"></script>
<div id = "root"></div>

<script src = "https://cdnjs.cloudflare.com/ajax/libs/redux-thunk/2.3.0/redux-thunk.min.js" integrity = "sha256-2xw5MpPcdu82/nmW2XQ6Ise9hKxziLWV2GupkS9knuw = " crossorigin = "anonymous"></script>
<script>var ReduxThunk = window.ReduxThunk.default</script>

Примечание: мы храним промежуточное состояние в изменяемые ссылки - stateRef.current = reducer(...), поэтому каждое промежуточное ПО может получить доступ к текущему, самому последнему состоянию во время его вызова с помощью getState.

Чтобы использовать точный API как useReducer, вы можете создать ловушку динамически:

const useMiddlewareReducer = createUseMiddlewareReducer(middlewares); //init Hook
const MyComp = () => { // later on in several components
  // ...
  const [state, dispatch] = useMiddlewareReducer(reducer, initState);
}

const middlewares = [ReduxThunk, logger];

const reducer = (state, { type, payload }) => {
  if (type === "loading") return { ...state, status: "loading" };
  if (type === "finished") return { status: "finished", data: payload };
  return state;
};

const initState = {
  status: "idle"
};

const AppContext = React.createContext();

const useMiddlewareReducer = createUseMiddlewareReducer(middlewares);

const AppContextProvider = ({ children }) => {
  const [state, dispatch] = useMiddlewareReducer(
    reducer,
    initState
  );
  return (
    <AppContext.Provider value = {{ state, dispatch }}>
      {children}
    </AppContext.Provider>
  );
};

function App() {
  return (
    <AppContextProvider>
      <Child />
    </AppContextProvider>
  );
}

const Child = () => {
  const val = React.useContext(AppContext);
  const {
    state: { status, data },
    dispatch
  } = val;
  return (
    <div>
      <p>Status: {status}</p>
      <p>Data: {data || "-"}</p>
      <button onClick = {() => dispatch(fetchData())}>Fetch data</button>
    </div>
  );
};

function fetchData() {
  return (dispatch, getState) => {
    dispatch({ type: "loading" });
    setTimeout(() => {
      // fake async loading
      dispatch({ type: "finished", payload: (getState().data || 0) + 42 });
    }, 2000);
  };
}

function logger({ getState }) {
  return next => action => {
    console.info("state:", JSON.stringify(getState()), "action:", JSON.stringify(action));
    return next(action);
  };
}

function createUseMiddlewareReducer(middlewares) {
  return (reducer, initState, initializer = s => s) => {
    const [state, setState] = React.useState(initializer(initState));
    const stateRef = React.useRef(state); // stores most recent state
    const dispatch = React.useMemo(
      () =>
        enhanceDispatch({
          getState: () => stateRef.current, // access most recent state
          stateDispatch: action => {
            stateRef.current = reducer(stateRef.current, action); // makes getState() possible
            setState(stateRef.current); // trigger re-render
            return action;
          }
        })(...middlewares),
      [middlewares, reducer]
    );
    return [state, dispatch];
  }
}

//                                                         |  dispatch fn  |
// A middleware has type (dispatch, getState) => nextMw => action => action
function enhanceDispatch({ getState, stateDispatch }) {
  return (...middlewares) => {
    let dispatch;
    const middlewareAPI = {
      getState,
      dispatch: action => dispatch(action)
    };
    dispatch = middlewares
      .map(m => m(middlewareAPI))
      .reduceRight((next, mw) => mw(next), stateDispatch);
    return dispatch;
  };
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity = "sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4 = " crossorigin = "anonymous"></script>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity = "sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w = " crossorigin = "anonymous"></script>
<div id = "root"></div>

<script src = "https://cdnjs.cloudflare.com/ajax/libs/redux-thunk/2.3.0/redux-thunk.min.js" integrity = "sha256-2xw5MpPcdu82/nmW2XQ6Ise9hKxziLWV2GupkS9knuw = " crossorigin = "anonymous"></script>
<script>var ReduxThunk = window.ReduxThunk.default</script>

Дополнительная информация - внешние библиотеки:react-use, react-hooks-global-state, react-enhanced-reducer-hook

В первом методе, поскольку dispatch является асинхронным, возможно, что он завершит действие намного позже. Как убедиться, что отправка завершена, прежде чем мы начнем получать данные?

Aditya Verma 04.09.2021 21:08

@AdityaVerma Вы не можете не усложнить ситуацию. Но зачем уменьшать воспринимаемую отзывчивость пользователя? По дизайну React этап асинхронной обработки прозрачен для разработчика. dispatch выполняются по порядку, поэтому вы всегда получаете loading раньше, чем finished, а сама диспетчеризация и чистый редуктор должны быть очень быстрыми, так как файлы. В худшем случае вы не видите loading.

ford04 07.09.2021 22:44

Решение 1 не имеет смысла, если ваши операции выборки включают обновленное состояние. Состояние, присвоенное операции, будет иметь исходное состояние, потому что процесс обновления выполняется асинхронно.

Delice 11.10.2021 14:41

Вы можете использовать пакет useAsync: https://github.com/sonofjavascript/use-async, который по сути является расширением ловушки useReducer, которая позволяет управлять асинхронными действиями над состоянием приложения через HTTP-запросы.

Установите агент клиента (вы можете использовать свой собственный http-клиент) через ClientStore:

import React from 'react'

import { ClientStore } from '@sonofjs/use-async'
import axios from 'axios'

import Component from './Component.jsx'

const ViewContainer = () => (
  <ClientStore.Provider agent = {axios}>
    <Component />
  </ClientStore.Provider>
)

export default ViewContainer

Определите и используйте свои действия:

import React, { useEffect }  from 'react'
import useAsync from '@sonofjs/use-async'

const actions = {
  FETCH_DATA: (state) => ({
    ...state,
    loading: true,
    request: {
      method: 'GET',
      url: '/api/data'
    }
  }),
  FETCH_DATA_SUCCESS: (state, response) => ({
    ...state,
    loading: false,
    data: response
  }),
  FETCH_DATA_ERROR: (state, error) => ({
    ...state,
    loading: false,
    error
  })
}

const initialState = {
  loading: false,
  data: {}
}

const Component = () => {
  const [state, dispatch] = useAsync(actions, initialState)

  useEffect(() => {
    dispatch({ type: 'DATA' })
  }, [])

  return (
    <>
      {state.loading ? <span>Loading...</span> : null}
      {<span>{JSON.stringify(state.data)}</span>}
      {state.error ? <span>Error: {JSON.stringify(state.error)}</span> : null}
    <>
  )
}

export default Component

Обновлять:

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

function useAsyncReducer(reducer, initState) {
    const [state, setState] = useState(initState),
        dispatchState = async (action) => setState(await reducer(state, action));
    return [state, dispatchState];
}

async function reducer(state, action) {
    switch (action.type) {
        case 'switch1':
            // Do async code here
            return 'newState';
    }
}

function App() {
    const [state, dispatchState] = useAsyncReducer(reducer, 'initState');
    return <ExampleComponent dispatchState = {dispatchState} />;
}

function ExampleComponent({ dispatchState }) {
    return <button onClick = {() => dispatchState({ type: 'switch1' })}>button</button>;
}

Старое решение:

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

Мое решение состояло в том, чтобы эмулировать useReducer, используя useState + асинхронную функцию:

async function updateFunction(action) {
    switch (action.type) {
        case 'switch1':
            // Do async code here (access current state with 'action.state')
            action.setState('newState');
            break;
    }
}

function App() {
    const [state, setState] = useState(),
        callUpdateFunction = (vars) => updateFunction({ ...vars, state, setState });

    return <ExampleComponent callUpdateFunction = {callUpdateFunction} />;
}

function ExampleComponent({ callUpdateFunction }) {
    return <button onClick = {() => callUpdateFunction({ type: 'switch1' })} />
}

это очень просто вы можете изменить состояние в useEffect после результата async Fuction

определить useState для результата выборки

const [resultFetch, setResultFetch] = useState(null);

и useEffect для прослушивания setResultFetch

после получения вызова асинхронного API setResultFetch(result of response)

useEffect(() => {
if (resultFetch) {
  const user = resultFetch;
  dispatch({ type: AC_USER_LOGIN, userId: user.ID})

}}, [resultFetch])

Другие вопросы по теме