Я пытаюсь получить некоторые данные с помощью нового 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 }
}
}
}
Возможно, полезно для тех, кто сталкивается с этим вопросом: robinwieruch.de/react-hooks-fetch-data





Это интересный случай, которого не затрагивают примеры 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>
);
}
но как я могу перезагрузить свой профиль из другого компонента с помощью переключателя редуктора? Раньше я передавал функцию выборки, которая изменяла значение в провайдере на верхнем уровне.
Я добавил несколько примеров предоставления дочерним компонентам метода перезагрузки данных в родительском элементе. Отвечает ли это на ваш вопрос?
да, спасибо, Работаю, когда я добавил перерыв; перезагрузить футляр!
Вы хотите избежать использования useEffect(async () => {}). Первый оператор return функции в useEffect предназначен для очистки, и это всегда немедленно возвращает обещание. Это будет предупреждать (и, возможно, не работать), когда хуки активны.
Хороший улов, Нейт! Я забыл про функцию очистки. Я обновил свой ответ, чтобы не возвращать Promise in useEffect.
вот очень хорошая статья, которая также применяет тот же подход medium.com/@patrick.gross1987/…
Не OP, но спасибо за этот ответ! Это было именно то, что я искал.
Не могу понять, почему в случае Перезагрузить из ребенка почему бы просто не использовать useState, поскольку поставщик контекста ведет себя так же, как редуктор?
Я написал очень подробное объяснение проблемы и возможных решений. Дэн Абрамов предложил Решение 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())
});
Полный пример кода находится здесь:
Это хорошая практика для держать редукторы в чистоте. Это сделает useReducer более предсказуемым и упростит тестирование. В обоих последующих подходах асинхронные операции сочетаются с чистыми редукторами:
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>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 является асинхронным, возможно, что он завершит действие намного позже. Как убедиться, что отправка завершена, прежде чем мы начнем получать данные?
@AdityaVerma Вы не можете не усложнить ситуацию. Но зачем уменьшать воспринимаемую отзывчивость пользователя? По дизайну React этап асинхронной обработки прозрачен для разработчика. dispatch выполняются по порядку, поэтому вы всегда получаете loading раньше, чем finished, а сама диспетчеризация и чистый редуктор должны быть очень быстрыми, так как файлы. В худшем случае вы не видите loading.
Решение 1 не имеет смысла, если ваши операции выборки включают обновленное состояние. Состояние, присвоенное операции, будет иметь исходное состояние, потому что процесс обновления выполняется асинхронно.
Вы можете использовать пакет 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])
Я думаю, вы хотите, чтобы ваш редуктор был синхронным. Возможно, вы могли бы установить значение, например,
loadingвtrueв случаеreloadи в вашем компоненте имеют эффект, который повторно запускается при измененииloading, напримерuseEffect(() => { if (loading) { reloadProfile().then(...) } }, [loading]);